diff --git a/doc/src/users-guide/ch-visualization.rst b/doc/src/users-guide/ch-visualization.rst index 4c1773cb4e7..1b332f19e90 100644 --- a/doc/src/users-guide/ch-visualization.rst +++ b/doc/src/users-guide/ch-visualization.rst @@ -489,8 +489,11 @@ Visualizing the Sky View ~~~~~~~~~~~~~~~~~~~~~~~~~~ The :ned:`GeoSkyViewCanvasVisualizer` draws an azimuth/elevation polar -plot, the "sky view", next to each observer node, showing where the target -nodes currently are in the observer's sky. It is primarily intended for +plot, the "sky view", for each observer node, showing where the target +nodes currently are in the observer's sky. By default the plots are tiled in a +grid below the map, each with the observer's name as its title; with finite +:par:`plotOffsetX` and :par:`plotOffsetY` each plot is drawn next to its +observer node instead. The visualizer is primarily intended for ground stations observing satellites. The nodes that get a plot are selected with the :par:`observerFilter` diff --git a/doc/whatsnew/compatible/visualizer-sky-view-title-format.txt b/doc/whatsnew/compatible/visualizer-sky-view-title-format.txt new file mode 100644 index 00000000000..9169f5cefd5 --- /dev/null +++ b/doc/whatsnew/compatible/visualizer-sky-view-title-format.txt @@ -0,0 +1,3 @@ +Sky view plot titles have a format + +The new titleFormat parameter of GeoSkyViewCanvasVisualizer selects the text of the title above each sky plot, the same way as labelFormat selects the label text of other visualizers. Its directives are %n (the observer node's full name), %N (its display name, or the full name if none is set) and %p (its full path). The default "%N" gives the title that this release introduces with the fix "Sky view plots name their observer": the display name of the observer node, or its full name if none is set. Simulation results do not change. diff --git a/doc/whatsnew/fixes/visualizer-sky-view-observer-titles.txt b/doc/whatsnew/fixes/visualizer-sky-view-observer-titles.txt new file mode 100644 index 00000000000..ec26937f0f7 --- /dev/null +++ b/doc/whatsnew/fixes/visualizer-sky-view-observer-titles.txt @@ -0,0 +1,3 @@ +Sky view plots name their observer + +GeoSkyViewCanvasVisualizer drew each observer's sky plot without a title, and in its default grid mode it tiles the plots below the map in observer order, so a plot could not be matched to its ground station. Each plot now has a title above it with the observer node's display name, or its full name if no display name is set. The grid rows are spaced one title line further apart, so a title does not touch the plot above it. The new displayTitles parameter turns the titles off and restores the old layout. Simulation results do not change. diff --git a/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.cc b/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.cc index 24f822c8be2..3d3128302b7 100644 --- a/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.cc +++ b/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.cc @@ -22,6 +22,8 @@ namespace visualizer { Define_Module(GeoSkyViewCanvasVisualizer); +static const double TITLE_HEIGHT = 14; // pixels between the plot rim and the bottom of the title above it + GeoSkyViewCanvasVisualizer::ObserverPlot::~ObserverPlot() { delete groupFigure; // deletes all child figures (plot decorations and target dots/labels/trails) @@ -54,6 +56,8 @@ void GeoSkyViewCanvasVisualizer::initialize(int stage) maskOpacity = par("maskOpacity"); cardinalColor = cFigure::parseColor(par("cardinalColor")); labelColor = cFigure::parseColor(par("labelColor")); + displayTitles = par("displayTitles"); + titleFormat.parseFormat(par("titleFormat")); targetColorSet.parseColors(par("targetColor")); targetRadius = par("targetRadius"); lowElevationOpacity = par("lowElevationOpacity"); @@ -309,9 +313,13 @@ void GeoSkyViewCanvasVisualizer::buildPlotDecorations(ObserverPlot *plot) addLabel("E", cFigure::Point(R + 1, 0), cFigure::ANCHOR_W, cardinalColor, 0); addLabel("W", cFigure::Point(-R - 1, 0), cFigure::ANCHOR_E, cardinalColor, 0); + // title above the N label: in grid mode the plots are away from the observer icons, + // so the title is what tells the plots apart + if (displayTitles) + addLabel(getObserverTitle(plot->observer).c_str(), cFigure::Point(0, -R - TITLE_HEIGHT), cFigure::ANCHOR_S, labelColor, 0); + // elevation labels sitting just above their ring along the North spoke, in a small - // (half-size) font; the 0 deg ring is the disc rim and is not labeled. No title is - // drawn: the observer module icon is right next to the plot and already shows its name. + // (half-size) font; the 0 deg ring is the disc rim and is not labeled for (double el : { 30.0, 60.0 }) { double r = R * (90 - el) / 90; char buf[8]; @@ -367,18 +375,40 @@ std::string GeoSkyViewCanvasVisualizer::getTargetLabel(const IMobility *target) return node != nullptr ? node->getFullName() : module->getFullName(); } +std::string GeoSkyViewCanvasVisualizer::getObserverTitle(const IMobility *observer) const +{ + auto module = check_and_cast(observer); + auto node = module->getParentModule() != nullptr ? module->getParentModule() : module; + std::function resolver = [&] (char directive) -> std::string { + switch (directive) { + case 'n': + return node->getFullName(); + case 'N': { + const char *displayName = node->getDisplayName(); + return displayName != nullptr && *displayName != '\0' ? displayName : node->getFullName(); + } + case 'p': + return node->getFullPath(); + default: + throw cRuntimeError("Unknown directive: %c", directive); + } + }; + return titleFormat.formatString(resolver); +} + void GeoSkyViewCanvasVisualizer::refreshDisplay() const { // when either offset is infinite, the plots are detached from the observer icons and tiled in a // grid below the target module's viewport; otherwise each plot is anchored to its observer icon bool gridMode = std::isinf(plotOffsetX) || std::isinf(plotOffsetY); double cell = 2 * plotRadius + plotRadius / 2; // plot diameter plus a gap + double rowTitleHeight = displayTitles ? TITLE_HEIGHT : 0; // the title line above each plot double gridX0 = 0, gridY0 = 0; int columns = 1; if (gridMode) { cFigure::Rectangle viewport = getViewportBounds(); gridX0 = viewport.x; - gridY0 = viewport.y + viewport.height + plotRadius / 2; // just below the viewport + gridY0 = viewport.y + viewport.height + plotRadius / 2 + rowTitleHeight; // just below the viewport columns = std::max(1, (int)std::floor(viewport.width / cell)); } @@ -390,7 +420,7 @@ void GeoSkyViewCanvasVisualizer::refreshDisplay() const // disc center, so seat the cell's top-left at the grid cell by offsetting by plotRadius int col = index % columns; int row = index / columns; - plot->groupFigure->setTransform(cFigure::Transform().translate(gridX0 + col * cell + plotRadius, gridY0 + row * cell + plotRadius)); + plot->groupFigure->setTransform(cFigure::Transform().translate(gridX0 + col * cell + plotRadius, gridY0 + row * (cell + rowTitleHeight) + plotRadius)); } else { auto observerModule = check_and_cast(plot->observer); diff --git a/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.h b/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.h index 8f078428b9d..92de4fc81d9 100644 --- a/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.h +++ b/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.h @@ -10,6 +10,7 @@ #include +#include "inet/common/StringFormat.h" #include "inet/common/figures/TrailFigure.h" #include "inet/common/geometry/common/GeographicCoordinateSystem.h" #include "inet/mobility/contract/IMobility.h" @@ -88,6 +89,8 @@ class INET_API GeoSkyViewCanvasVisualizer : public VisualizerBase, public cListe double maskOpacity = NaN; cFigure::Color cardinalColor; cFigure::Color labelColor; + bool displayTitles = false; + StringFormat titleFormat; ColorSet targetColorSet; double targetRadius = NaN; @@ -122,6 +125,9 @@ class INET_API GeoSkyViewCanvasVisualizer : public VisualizerBase, public cListe virtual cFigure::Point elevationAzimuthToCanvas(double elevationDeg, double azimuthDeg) const; virtual std::string getTargetLabel(const IMobility *target) const; + // the title of an observer's plot from titleFormat; override to name the observer from + // other data, as getTargetLabel() reads satelliteName + virtual std::string getObserverTitle(const IMobility *observer) const; virtual cFigure::Rectangle getViewportBounds() const; // the target module's drawing area, used to tile detached plots below it public: diff --git a/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.ned b/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.ned index 4a6a82c6205..b73421877e8 100644 --- a/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.ned +++ b/src/inet/visualizer/canvas/scene/GeoSkyViewCanvasVisualizer.ned @@ -19,7 +19,14 @@ import inet.visualizer.contract.IGeoSkyViewVisualizer; // rings mark elevations, the cardinal points (N/E/S/W) are drawn, and the band // below `elevationMask` is masked translucently. Each visible target is a // colored dot (optionally named, with a fading sky trail); targets below the -// horizon are hidden. +// horizon are hidden. By default, a title above each plot names its observer node: +// its display name, or its full name if no display name is set. In the grid layout +// the plots are 2.5 * `plotRadius` apart, so a wider title overlaps the next one. +// +// The title format string can contain the following directives: +// - %n observer node full name +// - %N observer node display name (the full name if no display name is set) +// - %p observer node full path // // Look angles are computed from the geographic positions of the observer and the // targets via the `IGeographicCoordinateSystem` given by `coordinateSystemModule`. @@ -50,6 +57,8 @@ simple GeoSkyViewCanvasVisualizer extends VisualizerBase like IGeoSkyViewVisuali double maskOpacity = default(0.25); // Opacity of the below-minimum-elevation mask string cardinalColor = default("#888888"); // Color of the N-S/W-E lines and the N/E/S/W labels string labelColor = default("black"); // Color of the title and elevation labels + bool displayTitles = default(true); // Display a title above each plot, with the text given by titleFormat + string titleFormat = default("%N"); // Determines what is displayed in the title, see the directives above string targetColor = default("dark"); // Target dot color, a list of colors or a color group name (e.g. dark, light) double targetRadius = default(3); // Radius of a target dot in pixels diff --git a/tests/fingerprint/store.json b/tests/fingerprint/store.json index d2233d166a2..42d425ce752 100644 --- a/tests/fingerprint/store.json +++ b/tests/fingerprint/store.json @@ -20515,8 +20515,8 @@ "sim_time_limit": "3600s", "test_result": "PASS", "ingredients": "tyf", - "fingerprint": "772e-b194", - "timestamp": 1792589940.0, + "fingerprint": "9471-891b", + "timestamp": 1793184170.0, "itervars": "$repetition==0" }, { diff --git a/tests/module/GeoSkyViewCanvasVisualizer_titles.test b/tests/module/GeoSkyViewCanvasVisualizer_titles.test new file mode 100644 index 00000000000..36614e3fc08 --- /dev/null +++ b/tests/module/GeoSkyViewCanvasVisualizer_titles.test @@ -0,0 +1,141 @@ +%description: + +Checks the titles of the GeoSkyViewCanvasVisualizer sky plots. Cmdenv runs with +its fake graphical user interface, so the visualizers draw. Three visualizers +watch the same two ground stations: gs[0] has the display name "Lima", gs[1] +has none. At the end of the run a checker prints the label texts of each sky +plot group on the network canvas, with the tags of its visualizer, and the +plot's center with the vertical position of each label relative to it: + - default: the title is the display name, or the full name if none is set (%N) + - format: titleFormat = "%p (%n)" gives the full path and the full name + - hidden: displayTitles = false draws no title + +The layout uses the default plotRadius of 70 pixels. The network's bgb of +300x200 pixels holds one 175-pixel grid column, so each visualizer's two plots +are in two rows below it. With titles, the title is 84 pixels above the plot +center (14 above the rim, 13 above the N label at -71), the first center is at +200 + 35 + 14 + 70 = 319, and the row pitch is 175 + 14 = 189, so a title stays +clear of the S label (+71) of the plot above it. Without titles, the centers are +at 305 and 480: the old row pitch of 175. + +%file: test.cc + +#include "inet/common/INETDefs.h" + +using namespace inet; + +namespace GeoSkyViewCanvasVisualizer_titles { + +class SkyViewLabelChecker : public cSimpleModule +{ + protected: + virtual void finish() override + { + auto canvas = getParentModule()->getCanvas(); + for (int i = 0; i < canvas->getRootFigure()->getNumFigures(); i++) { + auto group = dynamic_cast(canvas->getRootFigure()->getFigure(i)); + if (group == nullptr || strcmp(group->getName(), "skyView")) + continue; + std::string text = std::string("SKYVIEW ") + group->getTags() + ":"; + for (int j = 0; j < group->getNumFigures(); j++) + if (auto label = dynamic_cast(group->getFigure(j))) + text += std::string(" [") + label->getText() + "]"; + std::cout << text << std::endl; + // the plot center (the group's translation) and the label positions relative to it + const cFigure::Transform& transform = group->getTransform(); + std::cout << "LAYOUT " << group->getTags() << ": plot at " << transform.t1 << " " << transform.t2 << ";"; + for (int j = 0; j < group->getNumFigures(); j++) + if (auto label = dynamic_cast(group->getFigure(j))) + std::cout << " [" << label->getText() << " " << label->getPosition().y << "]"; + std::cout << std::endl; + } + } +}; + +Define_Module(SkyViewLabelChecker); + +} // namespace GeoSkyViewCanvasVisualizer_titles + +%file: test.ned + +import inet.common.geometry.common.Wgs84EcefGeographicCoordinateSystem; +import inet.mobility.contract.IMobility; +import inet.visualizer.canvas.scene.GeoSkyViewCanvasVisualizer; + +simple SkyViewLabelChecker +{ +} + +module GroundStation +{ + parameters: + @networkNode; + submodules: + mobility: like IMobility; +} + +network Test +{ + parameters: + @display("bgb=300,200"); + submodules: + coordinateSystem: Wgs84EcefGeographicCoordinateSystem; + skyViewDefault: GeoSkyViewCanvasVisualizer; + skyViewFormat: GeoSkyViewCanvasVisualizer; + skyViewHidden: GeoSkyViewCanvasVisualizer; + checker: SkyViewLabelChecker; + gs[2]: GroundStation; +} + +%inifile: omnetpp.ini + +[General] +network = Test +sim-time-limit = 1s +ned-path = .;../../../../src +cmdenv-fake-gui = true + +*.gs[0].display-name = "Lima" +*.gs[*].mobility.initFromDisplayString = false +*.gs[*].mobility.coordinateSystemModule = "^.^.coordinateSystem" +*.gs[0].mobility.initialLatitude = -12.046deg +*.gs[0].mobility.initialLongitude = -77.043deg +*.gs[1].mobility.initialLatitude = 34.052deg +*.gs[1].mobility.initialLongitude = -118.244deg + +*.skyView*.coordinateSystemModule = "^.coordinateSystem" +*.skyView*.observerFilter = "**.gs[*].mobility" +*.skyView*.targetFilter = "**.sat[*].mobility" +*.skyViewDefault.tags = "default" +*.skyViewFormat.tags = "format" +*.skyViewFormat.titleFormat = "%p (%n)" +*.skyViewHidden.tags = "hidden" +*.skyViewHidden.displayTitles = false + +%#-------------------------------------------------------------------------------------------------------------- +%# remove colorizing +%subst: /\x1B\[[0-9;]*m// +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +SKYVIEW sky_view default: [N] [S] [E] [W] [Lima] [30 +%contains: stdout +SKYVIEW sky_view default: [N] [S] [E] [W] [gs[1]] [30 +%contains: stdout +SKYVIEW sky_view format: [N] [S] [E] [W] [Test.gs[0] (gs[0])] [30 +%contains: stdout +SKYVIEW sky_view format: [N] [S] [E] [W] [Test.gs[1] (gs[1])] [30 +%contains: stdout +SKYVIEW sky_view hidden: [N] [S] [E] [W] [30 +%contains: stdout +LAYOUT sky_view default: plot at 70 319; [N -71] [S 71] [E 0] [W 0] [Lima -84] [30 +%contains: stdout +LAYOUT sky_view default: plot at 70 508; [N -71] [S 71] [E 0] [W 0] [gs[1] -84] [30 +%contains: stdout +LAYOUT sky_view hidden: plot at 70 305; [N -71] [S 71] [E 0] [W 0] [30 +%contains: stdout +LAYOUT sky_view hidden: plot at 70 480; [N -71] [S 71] [E 0] [W 0] [30 +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#--------------------------------------------------------------------------------------------------------------