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:

  • Zoom in + previous NOT capped → Reuse (new viewport is subset)
  • Zoom out or pan beyond bounds → Fetch new data
  • Filters changed → Fetch new data

Implementation details:

  • viewport cache stores bounds, stable non-bbox key, zoom, data, capped state, rendered stop IDs, and the last render zoom
  • cache key is built from non-bounding-box request params plus mode; limit, zoom, and include_meta are part of the key, while bbox coordinates and offset are excluded
  • 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 coordinates:

// Coordinates rounded to the configured tolerance grid
const tolerance = AppConstants.MARKERS.COORDINATE_TOLERANCE; // 0.00001
const key = `${Math.round(lat / tolerance) * tolerance},${Math.round(lon / tolerance) * tolerance}`;

// Overlapping markers offset in circle pattern
const angle = (index / total) * 2 * Math.PI;
const offsetLat = centerLat + radiusInDegrees * Math.sin(angle);
const offsetLon = centerLon + radiusInDegrees * Math.cos(angle);

This prevents markers from stacking invisibly.

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