Refine proxy endpoint map styling

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-24 19:32:03 +08:00
parent bbf3657ce7
commit 5732b059c8
4 changed files with 252 additions and 86 deletions
+32 -16
View File
@@ -14,23 +14,33 @@
height: 100%;
margin: 0;
overflow: hidden;
background: transparent;
background: var(--endpoint-surface);
}
:root {
--endpoint-attribution-background: rgb(255 255 255 / 92%);
--endpoint-attribution-foreground: #202020;
--endpoint-loading-background: rgb(255 255 255 / 32%);
--endpoint-loading-foreground: #202020;
--endpoint-surface: #f8fafd;
--endpoint-border: #d9e1ec;
--endpoint-text: #172033;
--endpoint-text-muted: #5e6b80;
--endpoint-marker-halo: rgb(23 32 51 / 24%);
--endpoint-attribution-background: rgb(248 250 253 / 92%);
--endpoint-loading-background: rgb(248 250 253 / 86%);
--endpoint-font-family: sans-serif;
--endpoint-font-size: 11pt;
}
:root[data-theme="dark"] {
--endpoint-attribution-background: rgb(24 30 39 / 92%);
--endpoint-attribution-foreground: #f5f5f5;
--endpoint-loading-background: rgb(16 21 28 / 38%);
--endpoint-loading-foreground: #f5f5f5;
--endpoint-surface: #11161d;
--endpoint-border: #2a3340;
--endpoint-text: #e7ecf4;
--endpoint-text-muted: #9aa7b8;
--endpoint-marker-halo: rgb(255 255 255 / 24%);
--endpoint-attribution-background: rgb(17 22 29 / 92%);
--endpoint-loading-background: rgb(17 22 29 / 86%);
}
body, #map, #endpoint-loading-overlay {
border-radius: 6px;
}
#endpoint-loading-overlay {
@@ -41,7 +51,7 @@
align-items: center;
justify-content: center;
gap: 8px;
color: var(--endpoint-loading-foreground);
color: var(--endpoint-text);
background: var(--endpoint-loading-background);
cursor: wait;
font-family: var(--endpoint-font-family);
@@ -71,13 +81,15 @@
}
.endpoint-marker {
width: 20px;
height: 20px;
width: 18px;
height: 18px;
box-sizing: border-box;
border: 3px solid white;
border: 2px solid white;
border-radius: 50%;
background: var(--endpoint-accent, #0f7bff);
box-shadow: 0 1px 5px rgb(0 0 0 / 45%);
box-shadow:
0 0 0 2px var(--endpoint-marker-halo),
0 2px 6px rgb(0 0 0 / 34%);
}
.maplibregl-canvas:focus,
@@ -87,9 +99,13 @@
.maplibregl-ctrl-attrib.maplibregl-compact {
max-width: calc(100% - 20px);
color: var(--endpoint-attribution-foreground) !important;
color: var(--endpoint-text-muted) !important;
background-color: var(--endpoint-attribution-background) !important;
box-shadow: 0 1px 4px rgb(0 0 0 / 24%);
border: 1px solid var(--endpoint-border);
border-radius: 7px 0 0 0;
box-shadow: none;
font-family: var(--endpoint-font-family);
font-size: 10px;
}
.maplibregl-ctrl-attrib a {
+98 -68
View File
@@ -6,7 +6,7 @@
let marker = null;
let state = null;
let appliedRevision = -1;
let activeDarkMode = null;
let activeStyleKey = '';
let initialReadyReported = false;
const loadingOverlay = document.getElementById('endpoint-loading-overlay');
const loadingText = document.getElementById('endpoint-loading-text');
@@ -24,40 +24,46 @@
],
];
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',
};
const MAP_PALETTES = {
dark: {
land: '#151a22',
landcover: '#19231f',
landuse: '#1a2028',
water: '#182734',
roadMinor: '#303a46',
roadMajor: '#465363',
building: '#202731',
waterLabel: '#8fa6b8',
},
light: {
land: '#f7f9fc',
landcover: '#eaf0e9',
landuse: '#edf1f5',
water: '#e2ebf2',
roadMinor: '#dce4ed',
roadMajor: '#becad8',
building: '#e4e9ef',
waterLabel: '#607d94',
},
};
const createPalette = (themeState) => {
const darkMode = Boolean(themeState.darkMode);
const mapPalette = MAP_PALETTES[darkMode ? 'dark' : 'light'];
return {
...mapPalette,
background: themeState.surfaceColor || (darkMode ? '#11161d' : '#f8fafd'),
boundary: themeState.borderColor || (darkMode ? '#2a3340' : '#d9e1ec'),
label: themeState.textColor || (darkMode ? '#e7ecf4' : '#172033'),
labelMuted: themeState.mutedTextColor || (darkMode ? '#9aa7b8' : '#5e6b80'),
halo: themeState.surfaceColor || (darkMode ? '#11161d' : '#f8fafd'),
poi: themeState.mutedTextColor || (darkMode ? '#9aa7b8' : '#5e6b80'),
};
};
const createStyle = (themeState) => {
const colors = createPalette(themeState);
return {
version: 8,
@@ -90,7 +96,7 @@
'source-layer': 'landcover',
paint: {
'fill-color': colors.landcover,
'fill-opacity': 0.55,
'fill-opacity': 0.3,
},
},
{
@@ -100,7 +106,7 @@
'source-layer': 'landuse',
paint: {
'fill-color': colors.landuse,
'fill-opacity': 0.5,
'fill-opacity': 0.32,
},
},
{
@@ -118,7 +124,7 @@
paint: {
'line-color': colors.boundary,
'line-width': 0.8,
'line-opacity': 0.7,
'line-opacity': 0.52,
},
},
{
@@ -128,9 +134,9 @@
'source-layer': 'transportation',
minzoom: 5,
paint: {
'line-color': colors.road,
'line-color': colors.roadMinor,
'line-width': 0.8,
'line-opacity': 0.65,
'line-opacity': 0.46,
},
},
{
@@ -157,7 +163,7 @@
14,
2.4,
],
'line-opacity': 0.85,
'line-opacity': 0.72,
},
},
{
@@ -165,10 +171,10 @@
type: 'fill',
source: 'openmaptiles',
'source-layer': 'building',
minzoom: 13,
minzoom: 14,
paint: {
'fill-color': colors.building,
'fill-opacity': 0.72,
'fill-opacity': 0.42,
},
},
{
@@ -196,6 +202,7 @@
'text-color': colors.waterLabel,
'text-halo-color': colors.halo,
'text-halo-width': 1,
'text-opacity': 0.76,
},
},
{
@@ -230,6 +237,7 @@
'text-color': colors.labelMuted,
'text-halo-color': colors.halo,
'text-halo-width': 1,
'text-opacity': 0.72,
},
},
{
@@ -237,7 +245,7 @@
type: 'symbol',
source: 'openmaptiles',
'source-layer': 'transportation_name',
minzoom: 14,
minzoom: 15,
filter: [
'match',
['get', 'class'],
@@ -256,6 +264,7 @@
'text-color': colors.labelMuted,
'text-halo-color': colors.halo,
'text-halo-width': 1,
'text-opacity': 0.58,
},
},
{
@@ -305,6 +314,7 @@
'text-color': colors.labelMuted,
'text-halo-color': colors.halo,
'text-halo-width': 1,
'text-opacity': 0.72,
},
},
{
@@ -347,7 +357,7 @@
type: 'symbol',
source: 'openmaptiles',
'source-layer': 'poi',
minzoom: 15,
minzoom: 16,
layout: {
'text-field': nameField,
'text-font': ['Noto Sans Regular'],
@@ -358,6 +368,7 @@
'text-color': colors.poi,
'text-halo-color': colors.halo,
'text-halo-width': 1,
'text-opacity': 0.58,
},
},
],
@@ -387,6 +398,40 @@
: '';
};
const applyThemeState = () => {
if (!state) {
return;
}
document.documentElement.dataset.theme = state.darkMode
? 'dark'
: 'light';
const properties = {
'--endpoint-accent': state.accentColor,
'--endpoint-surface': state.surfaceColor,
'--endpoint-border': state.borderColor,
'--endpoint-text': state.textColor,
'--endpoint-text-muted': state.mutedTextColor,
'--endpoint-font-family': JSON.stringify(state.fontFamily || 'sans-serif'),
'--endpoint-font-size': `${state.fontPointSize || 11}pt`,
};
for (const [name, value] of Object.entries(properties)) {
if (value) {
document.documentElement.style.setProperty(name, value);
}
}
};
const styleKey = () => JSON.stringify([
Boolean(state.darkMode),
state.surfaceColor,
state.borderColor,
state.textColor,
state.mutedTextColor,
]);
const reportError = (event) => {
const message = event && event.error && event.error.message
? event.error.message
@@ -426,36 +471,20 @@
const applyState = () => {
applyLoadingState();
applyThemeState();
if (!state) {
return;
}
document.documentElement.dataset.theme = state.darkMode
? 'dark'
: 'light';
document.documentElement.style.setProperty(
'--endpoint-accent',
state.accentColor
);
document.documentElement.style.setProperty(
'--endpoint-font-family',
JSON.stringify(state.fontFamily || 'sans-serif')
);
document.documentElement.style.setProperty(
'--endpoint-font-size',
`${state.fontPointSize || 11}pt`
);
if (!map) {
return;
}
const nextDarkMode = Boolean(state.darkMode);
if (nextDarkMode !== activeDarkMode) {
activeDarkMode = nextDarkMode;
const nextStyle = createStyle(nextDarkMode);
const nextStyleKey = styleKey();
if (nextStyleKey !== activeStyleKey) {
activeStyleKey = nextStyleKey;
const nextStyle = createStyle(state);
map.setStyle(nextStyle);
}
@@ -478,10 +507,10 @@
return;
}
activeDarkMode = Boolean(state.darkMode);
activeStyleKey = styleKey();
map = new maplibregl.Map({
container: 'map',
style: createStyle(activeDarkMode),
style: createStyle(state),
center: initialCoordinate,
zoom: state.defaultGeographicZoom,
minZoom: 2,
@@ -506,6 +535,7 @@
window.endpointMap = {
setState(nextState) {
state = nextState;
applyThemeState();
applyLoadingState();
createMap();
applyState();
+5
View File
@@ -417,6 +417,7 @@ class _EndpointMapWidget(QWidget):
return
hasLocation = self._hasLocation()
themePalette = AppStyleSheet.paletteForTheme(self._theme)
location, accentColor, font = (
self._location if hasLocation else None,
@@ -432,6 +433,10 @@ class _EndpointMapWidget(QWidget):
'viewRevision': self._viewRevision,
'darkMode': self._theme == AppStyleSheet.Dark,
'accentColor': accentColor,
'surfaceColor': themePalette['panel_alt'],
'borderColor': themePalette['border'],
'textColor': themePalette['text'],
'mutedTextColor': themePalette['muted'],
'loading': self._loading
or (
self._active
+117 -2
View File
@@ -54,6 +54,7 @@ from tests.support import (
import json
from pathlib import Path
import re
import unittest
from unittest.mock import patch
import weakref
@@ -752,6 +753,23 @@ class EndpointInfoServiceTest(unittest.TestCase):
widget.mapWidget._lastWebState['fontPointSize'],
QtGui.QFontInfo(widget.mapWidget.font()).pointSizeF(),
)
themePalette = AppStyleSheet.paletteForTheme(widget.mapWidget._theme)
self.assertEqual(
widget.mapWidget._lastWebState['surfaceColor'],
themePalette['panel_alt'],
)
self.assertEqual(
widget.mapWidget._lastWebState['borderColor'],
themePalette['border'],
)
self.assertEqual(
widget.mapWidget._lastWebState['textColor'],
themePalette['text'],
)
self.assertEqual(
widget.mapWidget._lastWebState['mutedTextColor'],
themePalette['muted'],
)
self.assertAlmostEqual(widget.mapWidget._lastWebState['markerLatitude'], 34.05)
self.assertAlmostEqual(
widget.mapWidget._lastWebState['markerLongitude'], -118.24
@@ -791,7 +809,7 @@ class EndpointInfoServiceTest(unittest.TestCase):
self.assertIn('map.setStyle(nextStyle)', mapScript)
self.assertIn("document.documentElement.dataset.theme", mapScript)
self.assertIn('--endpoint-attribution-background', html)
self.assertIn('--endpoint-attribution-foreground', html)
self.assertIn('--endpoint-text-muted', html)
self.assertIn('endpoint-loading-overlay', html)
self.assertIn('endpoint-loading-spinner', html)
self.assertIn('width: 16px', html)
@@ -805,7 +823,11 @@ class EndpointInfoServiceTest(unittest.TestCase):
self.assertIn('.maplibregl-canvas:focus', html)
self.assertIn('outline: none', html)
self.assertNotIn('color-mix(', html)
self.assertIn('const createStyle = (darkMode)', mapScript)
self.assertIn('const MAP_PALETTES = {', mapScript)
self.assertIn('const createPalette = (themeState)', mapScript)
self.assertIn('const createStyle = (themeState)', mapScript)
self.assertIn('style: createStyle(state)', mapScript)
self.assertIn('const styleKey = ()', 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)
@@ -891,6 +913,90 @@ class EndpointInfoServiceTest(unittest.TestCase):
widget.deleteLater()
service.deleteLater()
def testMapPaletteHasRestrainedThemeContrastAndLayerHierarchy(self):
"""Keep the local basemap readable without overpowering Fluent cards."""
mapScript = (
Path(__file__).parents[1]
/ 'Furious'
/ 'Data'
/ 'maplibre'
/ 'EndpointMap.js'
).read_text(encoding='utf-8')
def mapPalette(theme):
match = re.search(
rf"{theme}: \{{(?P<body>.*?)\n \}}",
mapScript,
re.DOTALL,
)
self.assertIsNotNone(match)
return dict(
re.findall(
r"(?P<name>[A-Za-z]+): '(?P<color>#[0-9a-f]{6})'",
match.group('body'),
)
)
darkMap = mapPalette('dark')
lightMap = mapPalette('light')
darkApp = AppStyleSheet.paletteForTheme(AppStyleSheet.Dark)
lightApp = AppStyleSheet.paletteForTheme(AppStyleSheet.Light)
for palette in (darkMap, lightMap):
self.assertEqual(
set(palette),
{
'land',
'landcover',
'landuse',
'water',
'roadMinor',
'roadMajor',
'building',
'waterLabel',
},
)
self.assertNotEqual(palette['water'], palette['land'])
self.assertNotEqual(palette['roadMinor'], palette['roadMajor'])
self.assertGreaterEqual(
abs(
QtGui.QColor(palette['water']).lightness()
- QtGui.QColor(palette['land']).lightness()
),
8,
)
self.assertGreater(
QtGui.QColor(darkApp['text']).lightness(),
QtGui.QColor(darkMap['land']).lightness(),
)
self.assertLess(
QtGui.QColor(lightApp['text']).lightness(),
QtGui.QColor(lightMap['land']).lightness(),
)
self.assertGreater(
abs(
QtGui.QColor(darkApp['accent']).lightness()
- QtGui.QColor(darkMap['land']).lightness()
),
60,
)
self.assertGreater(
abs(
QtGui.QColor(lightApp['accent']).lightness()
- QtGui.QColor(lightMap['land']).lightness()
),
60,
)
self.assertGreaterEqual(mapScript.count("'text-opacity': 0."), 5)
self.assertIn("id: 'poi-name'", mapScript)
self.assertIn("minzoom: 16", mapScript)
self.assertIn("id: 'building'", mapScript)
self.assertIn("minzoom: 14", mapScript)
self.assertEqual(mapScript.count('new maplibregl.Marker'), 1)
def testUnavailableLocationReplacesHtmlLoadingWithSingleLineFallback(self):
"""Hide web loading before presenting the terminal map fallback."""
service = _PresentationService()
@@ -1049,6 +1155,15 @@ class EndpointInfoServiceTest(unittest.TestCase):
widget.mapWidget._lastWebState['darkMode'],
theme == AppStyleSheet.Dark,
)
themePalette = AppStyleSheet.paletteForTheme(theme)
self.assertEqual(
widget.mapWidget._lastWebState['surfaceColor'],
themePalette['panel_alt'],
)
self.assertEqual(
widget.mapWidget._lastWebState['borderColor'],
themePalette['border'],
)
self.assertEqual(
(
widget.mapWidget._lastWebState['markerLatitude'],