mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-05 05:17:59 +03:00
Refine proxy endpoint map styling
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user