diff --git a/Furious/Data/maplibre/EndpointMap.js b/Furious/Data/maplibre/EndpointMap.js
index ef12f0d..21be8ab 100644
--- a/Furious/Data/maplibre/EndpointMap.js
+++ b/Furious/Data/maplibre/EndpointMap.js
@@ -6,11 +6,378 @@
let marker = null;
let state = null;
let appliedRevision = -1;
- let activeStyle = '';
+ let activeDarkMode = null;
let initialReadyReported = false;
const loadingOverlay = document.getElementById('endpoint-loading-overlay');
const loadingText = document.getElementById('endpoint-loading-text');
+ const nameField = [
+ 'case',
+ ['all', ['has', 'name:latin'], ['has', 'name:nonlatin']],
+ ['concat', ['get', 'name:latin'], '\n', ['get', 'name:nonlatin']],
+ [
+ 'coalesce',
+ ['get', 'name_en'],
+ ['get', 'name'],
+ ['get', 'name:latin'],
+ '',
+ ],
+ ];
+
+ const createStyle = (darkMode) => {
+ const colors = darkMode
+ ? {
+ background: '#0d1117',
+ land: '#161c25',
+ landcover: '#18271f',
+ landuse: '#1b222c',
+ water: '#19334a',
+ boundary: '#536174',
+ road: '#3e4a59',
+ roadMajor: '#657489',
+ building: '#222c38',
+ label: '#eef4fb',
+ labelMuted: '#b9c5d3',
+ halo: '#111720',
+ waterLabel: '#8fc4e8',
+ poi: '#c6d0dc',
+ }
+ : {
+ background: '#edf1f5',
+ land: '#f7f8fa',
+ landcover: '#e2ebdf',
+ landuse: '#eceff3',
+ water: '#bad9ec',
+ boundary: '#a7b0bd',
+ road: '#ffffff',
+ roadMajor: '#d4aa72',
+ building: '#d9dce1',
+ label: '#202832',
+ labelMuted: '#536170',
+ halo: '#ffffff',
+ waterLabel: '#446f91',
+ poi: '#4e5b68',
+ };
+
+ return {
+ version: 8,
+ glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
+ sources: {
+ openmaptiles: {
+ type: 'vector',
+ url: 'https://tiles.openfreemap.org/planet',
+ attribution: 'OpenFreeMap ' +
+ 'OpenStreetMap',
+ },
+ },
+ layers: [
+ {
+ id: 'background',
+ type: 'background',
+ paint: {'background-color': colors.background},
+ },
+ {
+ id: 'land',
+ type: 'fill',
+ source: 'openmaptiles',
+ 'source-layer': 'landcover',
+ paint: {'fill-color': colors.land},
+ },
+ {
+ id: 'landcover',
+ type: 'fill',
+ source: 'openmaptiles',
+ 'source-layer': 'landcover',
+ paint: {
+ 'fill-color': colors.landcover,
+ 'fill-opacity': 0.55,
+ },
+ },
+ {
+ id: 'landuse',
+ type: 'fill',
+ source: 'openmaptiles',
+ 'source-layer': 'landuse',
+ paint: {
+ 'fill-color': colors.landuse,
+ 'fill-opacity': 0.5,
+ },
+ },
+ {
+ id: 'water',
+ type: 'fill',
+ source: 'openmaptiles',
+ 'source-layer': 'water',
+ paint: {'fill-color': colors.water},
+ },
+ {
+ id: 'boundary',
+ type: 'line',
+ source: 'openmaptiles',
+ 'source-layer': 'boundary',
+ paint: {
+ 'line-color': colors.boundary,
+ 'line-width': 0.8,
+ 'line-opacity': 0.7,
+ },
+ },
+ {
+ id: 'transportation',
+ type: 'line',
+ source: 'openmaptiles',
+ 'source-layer': 'transportation',
+ minzoom: 5,
+ paint: {
+ 'line-color': colors.road,
+ 'line-width': 0.8,
+ 'line-opacity': 0.65,
+ },
+ },
+ {
+ id: 'transportation-major',
+ type: 'line',
+ source: 'openmaptiles',
+ 'source-layer': 'transportation',
+ minzoom: 6,
+ filter: [
+ 'match',
+ ['get', 'class'],
+ ['motorway', 'trunk', 'primary', 'secondary', 'tertiary'],
+ true,
+ false,
+ ],
+ paint: {
+ 'line-color': colors.roadMajor,
+ 'line-width': [
+ 'interpolate',
+ ['linear'],
+ ['zoom'],
+ 6,
+ 0.8,
+ 14,
+ 2.4,
+ ],
+ 'line-opacity': 0.85,
+ },
+ },
+ {
+ id: 'building',
+ type: 'fill',
+ source: 'openmaptiles',
+ 'source-layer': 'building',
+ minzoom: 13,
+ paint: {
+ 'fill-color': colors.building,
+ 'fill-opacity': 0.72,
+ },
+ },
+ {
+ id: 'water-name',
+ type: 'symbol',
+ source: 'openmaptiles',
+ 'source-layer': 'water_name',
+ minzoom: 3,
+ layout: {
+ 'symbol-placement': 'point',
+ 'text-field': nameField,
+ 'text-font': ['Noto Sans Italic'],
+ 'text-size': [
+ 'interpolate',
+ ['linear'],
+ ['zoom'],
+ 3,
+ 10,
+ 12,
+ 14,
+ ],
+ 'text-max-width': 8,
+ },
+ paint: {
+ 'text-color': colors.waterLabel,
+ 'text-halo-color': colors.halo,
+ 'text-halo-width': 1,
+ },
+ },
+ {
+ id: 'road-name-major',
+ type: 'symbol',
+ source: 'openmaptiles',
+ 'source-layer': 'transportation_name',
+ minzoom: 11,
+ filter: [
+ 'match',
+ ['get', 'class'],
+ ['motorway', 'trunk', 'primary', 'secondary', 'tertiary'],
+ true,
+ false,
+ ],
+ layout: {
+ 'symbol-placement': 'line',
+ 'symbol-spacing': 300,
+ 'text-field': nameField,
+ 'text-font': ['Noto Sans Regular'],
+ 'text-size': [
+ 'interpolate',
+ ['linear'],
+ ['zoom'],
+ 11,
+ 10,
+ 16,
+ 13,
+ ],
+ },
+ paint: {
+ 'text-color': colors.labelMuted,
+ 'text-halo-color': colors.halo,
+ 'text-halo-width': 1,
+ },
+ },
+ {
+ id: 'road-name-minor',
+ type: 'symbol',
+ source: 'openmaptiles',
+ 'source-layer': 'transportation_name',
+ minzoom: 14,
+ filter: [
+ 'match',
+ ['get', 'class'],
+ ['minor', 'service', 'track', 'path'],
+ true,
+ false,
+ ],
+ layout: {
+ 'symbol-placement': 'line',
+ 'symbol-spacing': 260,
+ 'text-field': nameField,
+ 'text-font': ['Noto Sans Regular'],
+ 'text-size': 11,
+ },
+ paint: {
+ 'text-color': colors.labelMuted,
+ 'text-halo-color': colors.halo,
+ 'text-halo-width': 1,
+ },
+ },
+ {
+ id: 'place-country',
+ type: 'symbol',
+ source: 'openmaptiles',
+ 'source-layer': 'place',
+ minzoom: 2,
+ maxzoom: 9,
+ filter: ['==', ['get', 'class'], 'country'],
+ layout: {
+ 'text-field': nameField,
+ 'text-font': ['Noto Sans Bold'],
+ 'text-size': [
+ 'interpolate',
+ ['linear'],
+ ['zoom'],
+ 2,
+ 10,
+ 7,
+ 16,
+ ],
+ 'text-max-width': 7,
+ },
+ paint: {
+ 'text-color': colors.label,
+ 'text-halo-color': colors.halo,
+ 'text-halo-width': 1.2,
+ },
+ },
+ {
+ id: 'place-state',
+ type: 'symbol',
+ source: 'openmaptiles',
+ 'source-layer': 'place',
+ minzoom: 4,
+ maxzoom: 10,
+ filter: ['==', ['get', 'class'], 'state'],
+ layout: {
+ 'text-field': nameField,
+ 'text-font': ['Noto Sans Italic'],
+ 'text-size': 11,
+ 'text-letter-spacing': 0.08,
+ 'text-max-width': 8,
+ },
+ paint: {
+ 'text-color': colors.labelMuted,
+ 'text-halo-color': colors.halo,
+ 'text-halo-width': 1,
+ },
+ },
+ {
+ id: 'place-city',
+ type: 'symbol',
+ source: 'openmaptiles',
+ 'source-layer': 'place',
+ minzoom: 3,
+ filter: [
+ 'match',
+ ['get', 'class'],
+ ['city', 'town', 'village'],
+ true,
+ false,
+ ],
+ layout: {
+ 'text-field': nameField,
+ 'text-font': ['Noto Sans Regular'],
+ 'text-size': [
+ 'interpolate',
+ ['linear'],
+ ['zoom'],
+ 4,
+ 11,
+ 12,
+ 15,
+ ],
+ 'text-max-width': 8,
+ 'text-variable-anchor': ['top', 'bottom', 'left', 'right'],
+ 'text-radial-offset': 0.4,
+ },
+ paint: {
+ 'text-color': colors.label,
+ 'text-halo-color': colors.halo,
+ 'text-halo-width': 1.2,
+ },
+ },
+ {
+ id: 'poi-name',
+ type: 'symbol',
+ source: 'openmaptiles',
+ 'source-layer': 'poi',
+ minzoom: 15,
+ layout: {
+ 'text-field': nameField,
+ 'text-font': ['Noto Sans Regular'],
+ 'text-size': 10,
+ 'text-max-width': 9,
+ },
+ paint: {
+ 'text-color': colors.poi,
+ 'text-halo-color': colors.halo,
+ 'text-halo-width': 1,
+ },
+ },
+ ],
+ };
+ };
+
+ const coordinate = () => {
+ if (!state || !state.markerVisible) {
+ return null;
+ }
+
+ const longitude = Number(state.markerLongitude);
+ const latitude = Number(state.markerLatitude);
+ if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
+ return null;
+ }
+
+ return [longitude, latitude];
+ };
+
const applyLoadingState = () => {
const loading = Boolean(state && state.loading);
loadingOverlay.dataset.visible = String(loading);
@@ -37,7 +404,8 @@
};
const ensureMarker = () => {
- if (!map || !state || !state.markerVisible) {
+ const markerCoordinate = coordinate();
+ if (!map || !markerCoordinate) {
if (marker) {
marker.remove();
marker = null;
@@ -52,7 +420,7 @@
}
marker
- .setLngLat([state.markerLongitude, state.markerLatitude])
+ .setLngLat(markerCoordinate)
.addTo(map);
};
@@ -84,18 +452,18 @@
return;
}
- const nextStyle = state.darkMode
- ? state.darkStyleUrl
- : state.lightStyleUrl;
- if (nextStyle !== activeStyle) {
- activeStyle = nextStyle;
+ const nextDarkMode = Boolean(state.darkMode);
+ if (nextDarkMode !== activeDarkMode) {
+ activeDarkMode = nextDarkMode;
+ const nextStyle = createStyle(nextDarkMode);
map.setStyle(nextStyle);
}
- if (state.viewRevision !== appliedRevision) {
+ const nextCoordinate = coordinate();
+ if (nextCoordinate && state.viewRevision !== appliedRevision) {
appliedRevision = state.viewRevision;
map.jumpTo({
- center: [state.markerLongitude, state.markerLatitude],
+ center: nextCoordinate,
zoom: state.defaultGeographicZoom,
});
}
@@ -105,20 +473,24 @@
};
const createMap = () => {
- if (map || !state || !state.markerVisible) {
+ const initialCoordinate = coordinate();
+ if (map || !initialCoordinate) {
return;
}
- activeStyle = state.darkMode
- ? state.darkStyleUrl
- : state.lightStyleUrl;
+ activeDarkMode = Boolean(state.darkMode);
map = new maplibregl.Map({
container: 'map',
- style: activeStyle,
- center: [state.markerLongitude, state.markerLatitude],
+ style: createStyle(activeDarkMode),
+ center: initialCoordinate,
zoom: state.defaultGeographicZoom,
minZoom: 2,
maxZoom: 18,
+ maxTileCacheSize: 64,
+ canvasContextAttributes: {
+ antialias: false,
+ powerPreference: 'low-power',
+ },
attributionControl: true,
cooperativeGestures: false,
fadeDuration: 0,
diff --git a/Furious/Widget/EndpointInfoWidget.py b/Furious/Widget/EndpointInfoWidget.py
index 301785f..9a07783 100644
--- a/Furious/Widget/EndpointInfoWidget.py
+++ b/Furious/Widget/EndpointInfoWidget.py
@@ -35,7 +35,6 @@ from PySide6.QtWebEngineCore import (
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWidgets import *
-import enum
import json
import logging
@@ -82,20 +81,7 @@ class _EndpointMapBridge(QtCore.QObject):
class _EndpointMapWidget(QWidget):
"""Host one lazily loaded MapLibre vector map for the page lifetime."""
- class Style(enum.StrEnum):
- """OpenFreeMap styles available to the embedded endpoint map."""
-
- Bright, Liberty, Positron, Dark, Fiord = (
- 'https://tiles.openfreemap.org/styles/bright',
- 'https://tiles.openfreemap.org/styles/liberty',
- 'https://tiles.openfreemap.org/styles/positron',
- 'https://tiles.openfreemap.org/styles/dark',
- 'https://tiles.openfreemap.org/styles/fiord',
- )
-
HtmlPath = DATA_DIR / 'maplibre' / 'EndpointMap.html'
- LightStyle = Style.Liberty
- DarkStyle = Style.Fiord
TrustedAttributionHosts = frozenset(
{
'openfreemap.org',
@@ -123,6 +109,7 @@ class _EndpointMapWidget(QWidget):
self._documentLoaded = False
self._mapReady = False
self._mapError = ''
+ self._rendererFailureHandled = False
self._viewRevision = 0
self._lastWebState = {}
self._retainMapDuringRefresh = False
@@ -350,10 +337,15 @@ class _EndpointMapWidget(QWidget):
"""Publish local-document failures and send the first map state."""
self._documentLoaded = bool(successful)
- if not successful:
+ if successful:
+ self._rendererFailureHandled = False
+ else:
self._mapError = self._unavailableText or 'Endpoint map unavailable'
- logger.error('failed to load the local endpoint map document')
+ # A renderer crash also completes the outstanding load with False.
+ # The termination handler already records that more precise cause.
+ if not self._rendererFailureHandled:
+ logger.error('failed to load the local endpoint map document')
self._syncWebState()
self._updateOverlay()
@@ -378,6 +370,11 @@ class _EndpointMapWidget(QWidget):
@QtCore.Slot(QWebEnginePage.RenderProcessTerminationStatus, int)
def _renderProcessTerminated(self, status, exitCode):
"""Convert an unexpected Chromium exit into the normal map fallback."""
+ if self._rendererFailureHandled:
+ return
+
+ self._rendererFailureHandled = True
+ self._documentLoaded = False
self._mapReady = False
self._mapError = self._unavailableText or 'Endpoint map unavailable'
@@ -416,8 +413,6 @@ class _EndpointMapWidget(QWidget):
'defaultGeographicZoom': self.DefaultGeographicZoom,
'viewRevision': self._viewRevision,
'darkMode': self._theme == AppStyleSheet.Dark,
- 'lightStyleUrl': self.LightStyle.value,
- 'darkStyleUrl': self.DarkStyle.value,
'accentColor': accentColor,
'loading': self._loading
or (
diff --git a/tests/test_endpoint_info.py b/tests/test_endpoint_info.py
index 50a19ab..e1471f5 100644
--- a/tests/test_endpoint_info.py
+++ b/tests/test_endpoint_info.py
@@ -805,26 +805,19 @@ class EndpointInfoServiceTest(unittest.TestCase):
self.assertIn('.maplibregl-canvas:focus', html)
self.assertIn('outline: none', html)
self.assertNotIn('color-mix(', html)
- self.assertEqual(
- {style.name: style.value for style in widget.mapWidget.Style},
- {
- 'Bright': 'https://tiles.openfreemap.org/styles/bright',
- 'Liberty': 'https://tiles.openfreemap.org/styles/liberty',
- 'Positron': 'https://tiles.openfreemap.org/styles/positron',
- 'Dark': 'https://tiles.openfreemap.org/styles/dark',
- 'Fiord': 'https://tiles.openfreemap.org/styles/fiord',
- },
- )
- self.assertEqual(
- widget.mapWidget._lastWebState['lightStyleUrl'],
- widget.mapWidget.LightStyle.value,
- )
- self.assertIs(widget.mapWidget.LightStyle, widget.mapWidget.Style.Liberty)
- self.assertEqual(
- widget.mapWidget._lastWebState['darkStyleUrl'],
- widget.mapWidget.DarkStyle.value,
- )
- self.assertIs(widget.mapWidget.DarkStyle, widget.mapWidget.Style.Fiord)
+ self.assertIn('const createStyle = (darkMode)', mapScript)
+ self.assertIn("url: 'https://tiles.openfreemap.org/planet'", mapScript)
+ self.assertIn("glyphs: 'https://tiles.openfreemap.org/fonts/", mapScript)
+ self.assertIn("'source-layer': 'water'", mapScript)
+ self.assertIn("'source-layer': 'building'", mapScript)
+ self.assertIn("'source-layer': 'transportation_name'", mapScript)
+ self.assertIn("'source-layer': 'place'", mapScript)
+ self.assertIn("'source-layer': 'poi'", mapScript)
+ self.assertIn("'text-field': nameField", mapScript)
+ self.assertIn('Number.isFinite(longitude)', mapScript)
+ self.assertNotIn('lightStyleUrl', widget.mapWidget._lastWebState)
+ self.assertNotIn('darkStyleUrl', widget.mapWidget._lastWebState)
+ self.assertNotIn('/styles/fiord', mapScript)
self.assertNotIn('tile.openstreetmap.org', html)
self.assertNotIn('opacity: 0.60', html)
self.assertNotIn('opacity: 0.60', mapScript)
@@ -948,10 +941,6 @@ class EndpointInfoServiceTest(unittest.TestCase):
self.assertEqual(widget.mapWidget._theme, AppStyleSheet.Light)
self.assertFalse(widget.mapWidget._lastWebState['darkMode'])
- self.assertEqual(
- widget.mapWidget._lastWebState['lightStyleUrl'],
- widget.mapWidget.LightStyle.value,
- )
self.assertTrue(scripts)
widget.close()