Map Data Loading and Rendering

The application efficiently loads, filters, and renders stop markers on the interactive map using spatial queries, caching, and marker diffing.

flowchart LR V["Viewport Change"] --> C{"Cache Valid?"} C -->|Yes| R["Render from Cache"] C -->|No| A["API Request"] A --> P["PostGIS Query<br/>ST_Intersects"] P --> D["Diff Markers"] D --> R

Data Pipeline

Step Component Purpose
1 PostGIS Spatial index queries via GiST
2 API /api/data with viewport bounds
3 Cache Avoid redundant requests
4 Diff Update only changed markers

PostGIS Spatial Query

SELECT * FROM stops_matched
WHERE ST_Intersects(
    geom,
    ST_MakeEnvelope(min_lon, min_lat, max_lon, max_lat, 4326)
)

The real /api/data query also includes a second branch for matched rows whose OSM point is inside the viewport even when the ATLAS-side display geometry is outside it. The geom column has a GiST spatial index for the primary lookup path.

Zoom Level Modes

Zoom Mode Behavior
< 13 Overview / Low Zoom Unmatched ATLAS only (if no filters) or capped filtered results
13–14 Capped / Mid Zoom All types (or filters), limited to a capped count to maintain performance
≥ 15 Full / Uncapped Zoom All filtered viewport rows rendered without an artificial result cap

Zoom Banner Policy

The application intelligently restricts the number of rendered markers to maintain performance, and communicates this state via the map's zoom banner.

  • High Zoom (≥ 15): The banner is hidden. The data isn't artificially capped.
  • Mid-Zoom (13-14): Results are capped (currently 1,800 rows). If filters are inactive, the banner advises the user to zoom in. If filters are active, it indicates that results are capped (for example, Showing first 1800 filtered results...). If the real query size naturally drops below the cap, the banner is hidden.
  • Low Zoom (< 13): By default, the map runs an "Overview mode" displaying only unmatched ATLAS components to save resources. The banner indicates this mode. If explicit active filters are applied, the map respects the filter instead of Overview mode, applies the same cap, and updates the banner accordingly.
  • Top N Filters: Skip full viewport loading and hide the zoom banner, since the data slice is loaded through the targeted Top N overlay path.

Frontend Caching

Cache reuse logic:

  • Same zoom + viewport remains inside the buffered request bounds → Reuse
  • Zoom in + previous response was not capped + viewport remains inside the buffered request bounds → Reuse
  • Zoom out, a capped zoom change, or movement beyond the buffered request bounds → Fetch new data
  • Filters changed → Fetch new data

Implementation details:

  • MapViewportLoader stores the last response data, visible bounds, buffered request bounds, zoom, and request identity
  • the request identity is built from the viewport policy's non-bounding-box parameters plus its mode; this includes offset, zoom, include_meta, and any active limit
  • bounding-box coordinates are kept separately in the buffered request bounds and are not part of the identity
  • capped state is derived from the cached response metadata when deciding whether a zoom-in reuse is safe
  • the cache is invalidated explicitly on filter changes via invalidateViewportCache()

Cache ownership boundaries:

  • this cache applies only to /api/data viewport payload reuse in the browser
  • /api/global_stats is requested independently by the header summary and is not served from viewport cache
  • both request paths are driven by the same active filter semantics, so viewport data and summary counts stay logically aligned

Marker Diffing

Instead of recreating all markers:

  1. Track markers by key (atlas-{sloid} or osm-{node_id})
  2. Existing: Update position with setLatLng()
  3. New: Create and add to map
  4. Stale: Remove from map

Benefits: No flickering, smooth transitions, efficient DOM operations

Canvas vs DOM Markers

Zoom Marker Type Implementation
< 18 L.circleMarker Lightweight circle rendering
≥ 18 L.marker for labeled D/P/S markers; L.circleMarker otherwise Label-capable categories switch to DOM icons, while ordinary circles keep the same circle marker implementation

At zoom 18, markers are recreated so duplicate ATLAS markers and selected OSM marker types can switch to their labeled icon form.

Labeled Marker Policy (D / P / S)

When marker rendering is in DOM mode (zoom >= 18), selected marker categories render a lettered icon:

Side Condition Label
ATLAS API has_atlas_duplicate flag is set from AtlasStop.duplicate_group_sloids D
OSM osm_node_type = platform P
OSM osm_node_type = railway_station S

All other marker categories keep the standard circle style.

Marker Colors

Type Color
Matched ATLAS Navy (#174092)
Matched OSM Green (#4CAF50)
Unmatched ATLAS Red (#DC3545)
Unmatched OSM Gray (#6C757D)

Line Rendering Thresholds

Line Type Min Zoom
ATLAS↔OSM matched connection lines 13
OSM group / trio connector lines 17

Overlap Handling

The MarkerClusterManager handles stops at identical or near-identical coordinates. When a Leaflet map projection is available, it projects coordinates at a stable reference zoom, groups nearby points using a spatial grid, and offsets each group in pixel space:

const point = map.project([lat, lon], referenceZoom);
const cellX = Math.floor(point.x / overlapDistance);
const cellY = Math.floor(point.y / overlapDistance);

const angle = (2 * Math.PI * index) / groupSize;
const displayPoint = {
    x: point.x + offsetRadius * Math.cos(angle),
    y: point.y + offsetRadius * Math.sin(angle)
};
const displayLatLng = map.unproject(displayPoint, referenceZoom);

Entries are sorted by entity type and stable key before layout, which keeps displacement deterministic. The configured coordinate tolerance (0.00001) is only the fallback grouping threshold when map projection is unavailable. At lower zooms, subpixel offsets are skipped because they would not be visible.

Map Layers

The interactive map provides 3 layer options.

Layer URL
OpenStreetMap tile.openstreetmap.org
Transport tile.memomaps.de
Satellite arcgisonline.com

Code Reference

File Purpose
backend/blueprints/data.py API endpoint
static/js/pages/main.js Map initialization
static/js/components/map-renderer.js Marker rendering
matching_and_import_db/database/importer.py Geometry import

Related Documentation

Data update in progress
Elapsed: -- ETA: -- Phase: idle