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.
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-bboxkey,zoom,data,cappedstate, rendered stop IDs, and the last render zoom - cache key is built from non-bounding-box request params plus mode;
limit,zoom, andinclude_metaare part of the key, while bbox coordinates andoffsetare excluded - the cache is invalidated explicitly on filter changes via
invalidateViewportCache()
Cache ownership boundaries:
- this cache applies only to
/api/dataviewport payload reuse in the browser /api/global_statsis 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:
- Track markers by key (
atlas-{sloid}orosm-{node_id}) - Existing: Update position with
setLatLng() - New: Create and add to map
- 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
- 6. Web app – Application overview
- 5. Database – Database schema