Skip to content

Map: scale bar on the 2D map and in PNG/PDF exports - #574

Draft
sandesh-sp wants to merge 6 commits into
developmentfrom
feature/570-scale-bar
Draft

sandesh-sp wants to merge 6 commits into
developmentfrom
feature/570-scale-bar

Conversation

@sandesh-sp

@sandesh-sp sandesh-sp commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #570

Summary

  • ScaleBar tool (src/essence/Tools/ScaleBar/) shows a metric or imperial scale bar, by default in a float-bottom-left panel. The bar is labelled with a round distance (1, 2, 5, 10, …) and switches between m/km or ft/mi.
  • Scale under the pointer. The bar gives the scale where the pointer is on the map, and at the map's centre while the pointer is off the map (or over empty space past a zoomed-out Web Mercator world). A scale at one point holds at any zoom and tilt, so the bar always shows.
  • Measured on screen, for any engine. mapScale.ts unprojects two points a few pixels apart and measures the great-circle distance between them on the mission's planet. It works for deck.gl and Leaflet alike, and for non-Mercator projections.
  • Bus API: map:getMetersPerPixel takes an optional { x, y } map container point and returns ground metres per CSS pixel there (the map's centre when omitted). map:mouseout reports the pointer leaving the map; deck.gl raises it on the container's mouseleave, Leaflet natively. Documented in Event-Bus-API.md.
  • Exports: while the bar is on screen, Share's PNG and PDF exports draw a matching bar at the same spot, measured there. map:getScreenshot accepts { scaleAt } and returns metersPerPixel in image pixels plus the scaleAnchor, read before the capture starts, so the bar is correct on high-DPI displays.
  • Refresh: the tool reads the scale on handler-ready, every map:move and map:moveend, pointer moves, and window resize (once per frame). Reads that would draw the same bar skip the render, and stale reads that settle out of order are dropped.
  • deck.gl emits move on every camera frame: overlay mode, standalone mode, and side-by-side pane drags. map:move subscribers and addOverlay's reposition listener now track the camera during a gesture. Projections share one viewport per camera.

Behaviour and limits

  • Exports keep view-wide rules, since an image cannot show where the scale applies: no bar when the map is tilted more than 10°, or when the scale varies more than 1.5× down the view (sampled on five rows). The second rule hides the exported bar at continent- and world-wide views (about zoom ≤ 2 at the equator, ≤ 4 at mid-latitudes on an 800×600 map) and never lets it return as the map zooms out further.
  • No bar when the map has no size yet or the planet radius is not configured.
  • Exports draw a bar exactly when the tool's bar is on screen (plugin:scalebar:getSettings). Making this a plugin-neutral contract is deferred to the plugin-to-plugin communication work.
  • Lifting the bottom float zones above the basemap's logo and attribution is split out to a follow-up branch (feature/570-map-chrome-insets), since it changes every bottom float panel. Until it lands, a bottom-left bar can sit close to the MapLibre logo.

Test plan

  • npx vitest run: 215 files, 3,666 tests pass
  • Move the pointer from the bottom to the top of a deck.gl and a Leaflet map and check the bar's label follows the latitude under it
  • Move the pointer off the map, and over empty space above a zoomed-out world, and check the bar shows the centre's scale
  • Scroll-zoom with the pointer still and check the bar keeps up frame by frame
  • Tilt the map past 10° and check the bar still shows, while a PNG export carries none
  • Zoom out to a world view, export PNG, and check the export has no bar; zoom in and check it returns
  • Drag a side-by-side comparison pane and check the bar updates during the drag
  • Export PNG and PDF on a high-DPI display and check the exported bar matches the on-screen bar and sits at the same spot
  • Check HTML overlays (addOverlay) still track the camera while panning in overlay mode

Adds a ScaleBar tool that shows a metric or imperial scale bar over the
map, and draws a matching bar, measured in image pixels, onto Share
exports while the tool is in the layout.

- IMapEngine gains getMetersPerPixel() and getChromeInsets(); the
  deck.gl adapter derives the scale from Web Mercator at the view
  centre, and the Leaflet adapter reports none.
- Screenshots carry metersPerPixel in image pixels, so exports scale
  correctly on high-DPI displays.
- Bottom float zones sit above the basemap's logo and attribution.
The map scale is the ground distance between two nearby screen points,
measured on the mission's planet, so it works for deck.gl and Leaflet
alike and holds at the bar's own position rather than the view centre.

- mapScale.ts measures metres per CSS pixel at a page position (or near
  the bottom-left corner) and stamps screenshots with the scale in image
  pixels, read before the capture starts.
- map:getMetersPerPixel takes an optional { clientX, clientY }; the
  ScaleBar asks at its own position.
- Remove the Web Mercator formula and getMetersPerPixel from IMapEngine
  and the adapters.
- No scale while the map is tilted more than 10 degrees, where no single
  bar fits the view.
- Indent the bar's label 8px on screen and in exports.
The bottom float zones' clearance above the basemap logo and attribution
is a layout change for every bottom panel, so it moves to a separate
branch.
- Read the scale on handler-ready, map:moveend and window resize, and at
  most once per frame on map:move, instead of retrying null reads on a
  timer.
- Provide the export settings with units as an effect dependency.
- ScaleBarSettings reuses ScaleUnits; LegendDeps becomes DecorationDeps.
- Drop the unused tuple branch in metersPerPixelAt.
- Shorten the mapScale docstrings, the bus API row and the tool's
  Configure description.
- The on-screen bar gives the scale under the pointer, or at the map's
  centre while the pointer is off the map or off a Web Mercator world. It
  shows at any zoom and tilt, since the scale at one point holds there.
- map:getMetersPerPixel takes a container point { x, y }, centre by
  default. Engines report the pointer leaving the map as 'mouseout'
  (deck.gl on container mouseleave), re-emitted as map:mouseout.
- Exports measure where the on-screen bar sits and carry no bar when the
  map is tilted past 10° or the scale varies more than 1.5x down the view,
  sampled on five rows so a world view never passes.
- The bar re-reads on every map:move, skips renders that would look the
  same, and ignores reads that settle out of order.
- deck.gl emits 'move' during side-by-side pane drags and standalone
  camera changes, and shares one viewport per camera for projections.
…al export

- deck.gl without a basemap reports jumps made in code, and raises
  'moveend' once the camera settles instead of on every frame
- On Leaflet the bar measures at the map centre, since Leaflet can miss
  the pointer leaving the map across a hoverable feature
- Engines report Web Mercator through IMapEngine.isWebMercator() rather
  than a global another engine may have left behind
- Exports ask 'export:getScaleBar', which answers only while a bar is
  drawn and anchors at the bar's backing box
- The adapter keeps the built model in state; exports without a scale by
  design no longer log
- Document the map pointer events, map:getScreenshot and
  export:getScaleBar
@sandesh-sp
sandesh-sp marked this pull request as draft October 20, 2026 14:14

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Map: add a scale bar to the 2D map and include it in PNG/PDF exports

1 participant