Skip to content

Commit a90cbda

Browse files
authored
MapLibre for OSM map (#95)
* Move OSM to maplibre-ml * Remove leaflet + OSMMap * Refactors * Refactors
1 parent e6e20a2 commit a90cbda

10 files changed

Lines changed: 97 additions & 258 deletions

File tree

‎examples/.env‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ ADMIN_PASSWORD=my-secret-password
1212
STORAGE_TYPE=sqlite
1313
# Where to store application data
1414
DATA_DIR=/tmp
15-
# Map type to use: openstreetmaps or maptiler (requires API key but looks WAY nicer)
16-
MAP_TYPE=openstreetmaps
15+
# Map type to use: openstreetmap or maptiler (requires API key but looks WAY nicer)
16+
MAP_TYPE=openstreetmap
1717
# Must be set if using maptiler; see https://www.maptiler.com/
1818
MAPTILER_API_KEY=
1919

‎ui/Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ build: install build-protos
4747

4848
.PHONY: init-local-env
4949
init-local-env:
50-
@printf "VITE_PROXY_URL=http://localhost:9123\nVITE_MAP_TYPE=openstreetmaps\n" > .env.local
50+
@printf "VITE_PROXY_URL=http://localhost:9123\nVITE_MAP_TYPE=openstreetmap\n" > .env.local
5151

5252
.PHONY: run
5353
run: build

‎ui/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The UI currently supports 2 map types (controlled by the `MAP_TYPE` env var):
1010
| [MapTiler](https://www.maptiler.com/) | Uses vector tiling and looks a lot nicer. Requires a free API key - see [here](https://www.maptiler.com/cloud/pricing/) for more info. |
1111

1212
## Features
13-
* **Interactive Maps:** powered by [MapLibre](https://maplibre.org/) / [MapTiler](https://www.maptiler.com/) / [OpenStreetMap](https://www.openstreetmap.org/) / [Leaflet](https://leafletjs.com/).
13+
* **Interactive Maps:** powered by [MapLibre](https://maplibre.org/) / [MapTiler](https://www.maptiler.com/) / [OpenStreetMap](https://www.openstreetmap.org/).
1414
* **Timeline View:** view your location history for multiple devices.
1515
* **Device Management:** view status of connected trackers.
1616

‎ui/package-lock.json‎

Lines changed: 0 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ui/package.json‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,15 @@
1414
"@turf/circle": "^7.3.2",
1515
"antd": "^6.1.4",
1616
"date-fns": "^4.1.0",
17-
"leaflet": "^1.9.4",
1817
"maplibre-gl": "^5.15.0",
1918
"nice-grpc-web": "^3.3.9",
2019
"react": "^19.2.0",
2120
"react-dom": "^19.2.0",
22-
"react-leaflet": "^5.0.0",
2321
"react-map-gl": "^8.1.0",
2422
"sonner": "^2.0.7"
2523
},
2624
"devDependencies": {
2725
"@eslint/js": "^9.39.1",
28-
"@types/leaflet": "^1.9.21",
2926
"@types/node": "^24.10.1",
3027
"@types/react": "^19.2.5",
3128
"@types/react-dom": "^19.2.3",

‎ui/src/App.css‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,6 @@ html, body, #root {
159159
pointer-events: none; /* Allows clicking through to map if needed */
160160
}
161161

162-
.leaflet-container {
163-
height: 100%;
164-
width: 100%;
165-
}
166-
167-
.hide-while-flying .leaflet-overlay-pane {
168-
opacity: 0;
169-
transition: opacity 0.2s; /* Optional: smooth fade out */
170-
}
171-
172162
.logout-button {
173163
padding-top: 50px;
174164
padding-left: 10px;

‎ui/src/App.tsx‎

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,11 @@ import {useAdminClient} from './hooks/use-admin-client.ts';
33
import {Login} from "./components/Login.tsx";
44
import {DeviceList} from "./components/DeviceList.tsx";
55
import {useLogin} from "./hooks/use-login.ts";
6-
import {MLMap} from "./components/MLMap.tsx";
7-
import {MAP_TYPE} from "./config/config.ts";
8-
import {OSMMap} from "./components/OSMMap.tsx";
9-
import type {StoredLocation} from "./gen/common.ts";
106
import {useMemo, useState} from "react";
117
import {usePushPoller} from "./hooks/use-push-poller.ts";
128
import {usePageVisibility} from "./hooks/use-page-visibility.ts";
139
import {Toaster} from "sonner";
14-
15-
const DisplayMap = ({history, selectedDeviceId, forceRecenter, setForceRecenter}: {
16-
history: StoredLocation[],
17-
selectedDeviceId: string | null,
18-
forceRecenter: boolean,
19-
setForceRecenter: (forceRecenter: boolean) => void,
20-
}) => {
21-
switch (MAP_TYPE) {
22-
case "maptiler":
23-
return (
24-
<MLMap
25-
history={history}
26-
selectedDeviceId={selectedDeviceId}
27-
forceRecenter={forceRecenter}
28-
setForceRecenter={setForceRecenter}
29-
/>
30-
)
31-
case "openstreetmaps":
32-
return (
33-
<OSMMap
34-
history={history}
35-
selectedDeviceId={selectedDeviceId}
36-
forceRecenter={forceRecenter}
37-
setForceRecenter={setForceRecenter}
38-
/>
39-
)
40-
default:
41-
alert("Invalid map type " + MAP_TYPE + ", must be one of: [maptiler, openstreetmaps]");
42-
return null;
43-
}
44-
}
10+
import {MLMap} from "./components/MLMap.tsx";
4511

4612
const Dashboard = () => {
4713
const REFRESH_INTERVAL = 10_000;
@@ -105,10 +71,12 @@ const Dashboard = () => {
10571
sendNotification={sendNotification}
10672
logout={logout}/>
10773

108-
<DisplayMap history={history}
109-
selectedDeviceId={selectedDeviceId}
110-
forceRecenter={forceRecenter}
111-
setForceRecenter={setForceRecenter}/>
74+
<MLMap
75+
history={history}
76+
selectedDeviceId={selectedDeviceId}
77+
forceRecenter={forceRecenter}
78+
setForceRecenter={setForceRecenter}
79+
/>
11280
</div>
11381
);
11482
};

‎ui/src/components/MLMap.tsx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {Segmented} from "antd";
99
import {useLocalStorage} from "../hooks/use-local-storage.ts";
1010
import styles from './MLMap.module.css';
1111
import {accuracyCircleStyle, circlePoint, lineStyle, pointStyle} from "./MLMapStyles.tsx";
12-
import {DEFAULT_CENTER, DEFAULT_ZOOM, getMapUrl, MAP_STYLE_OPTIONS, POINT_LIMIT} from "./MLMapConfig.tsx";
12+
import {DEFAULT_CENTER, DEFAULT_ZOOM, getMapUrl, mapStyleOptions, MapType, POINT_LIMIT} from "./MLMapConfig.tsx";
1313
import {MapUpdater} from "./MLMapUpdater.tsx";
14+
import {MAP_TYPE} from "../config/config.ts";
1415

1516
interface MLMapProps {
1617
history: StoredLocation[];
@@ -22,10 +23,10 @@ interface MLMapProps {
2223
export const MLMap: React.FC<MLMapProps> = ({history, selectedDeviceId, forceRecenter, setForceRecenter}) => {
2324
const [popupInfo, setPopupInfo] = useState<MapGeoJSONFeature | null>(null);
2425
const [cursor, setCursor] = useState('');
25-
const [mapStyle, setMapStyle] = useLocalStorage("ml_map_style", MAP_STYLE_OPTIONS[0].value);
26+
const [mapStyle, setMapStyle] = useLocalStorage("ml_map_style", mapStyleOptions(MAP_TYPE)[0].value);
2627
const [currentTime, setCurrentTime] = useState(() => Date.now());
2728

28-
const mapUrl = useMemo(() => getMapUrl(mapStyle), [mapStyle]);
29+
const mapUrl = useMemo(() => getMapUrl(MAP_TYPE as MapType, mapStyle), [mapStyle]);
2930

3031
const lastLocation: StoredLocation | null = history.length > 0 ? history[history.length - 1] : null;
3132
const mapCenter: [number, number] = lastLocation != null && lastLocation.location != null ?
@@ -104,7 +105,7 @@ export const MLMap: React.FC<MLMapProps> = ({history, selectedDeviceId, forceRec
104105
<Segmented
105106
vertical
106107
className={styles.segmentedControl}
107-
options={MAP_STYLE_OPTIONS}
108+
options={mapStyleOptions(MAP_TYPE as MapType)}
108109
value={mapStyle}
109110
onChange={setMapStyle}
110111
/>
@@ -129,7 +130,7 @@ export const MLMap: React.FC<MLMapProps> = ({history, selectedDeviceId, forceRec
129130
zoom: DEFAULT_ZOOM
130131
}}
131132
style={{width: '100%', height: '100%'}}
132-
mapStyle={mapUrl}
133+
mapStyle={mapUrl as any}
133134
interactiveLayerIds={['history-points']}
134135
cursor={cursor}
135136
onMouseEnter={() => setCursor('pointer')}

‎ui/src/components/MLMapConfig.tsx‎

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,87 @@
11
import {MAPTILER_API_KEY} from "../config/config.ts";
22
import {GlobalOutlined, MoonFilled, SunOutlined} from "@ant-design/icons";
33

4-
if (!MAPTILER_API_KEY) {
5-
alert("MAPTILER_API_KEY must be set to use maptiler");
6-
}
7-
8-
export const getMapUrl = (style: string) => {
9-
return `https://api.maptiler.com/maps/${style}/style.json?key=${MAPTILER_API_KEY}`;
10-
}
114
export const DEFAULT_CENTER: [number, number] = [40, 0];
125
export const DEFAULT_ZOOM = 2;
136
export const DEFAULT_ZOOM_IN = 15;
14-
export const MAP_STYLE_OPTIONS = [
15-
{value: "streets-v2", label: <SunOutlined/>},
16-
{value: "base-v4-dark", label: <MoonFilled/>},
17-
{value: "satellite", label: <GlobalOutlined/>},
18-
];
197

20-
export const POINT_LIMIT = 25;
8+
export const POINT_LIMIT = 25;
9+
10+
export enum MapType {
11+
Maptiler = "maptiler",
12+
OSM = "openstreetmap"
13+
}
14+
15+
export enum MapStyle {
16+
Light = "LIGHT",
17+
Dark = "DARK",
18+
Satellite = "SATELLITE",
19+
}
20+
21+
export const mapStyleOptions = (mapType: MapType) => {
22+
const options = [
23+
{value: MapStyle.Light, label: <SunOutlined/>},
24+
{value: MapStyle.Dark, label: <MoonFilled/>},
25+
];
26+
27+
// Satellite only support for maptiler
28+
if (mapType === MapType.Maptiler) options.push({value: MapStyle.Satellite, label: <GlobalOutlined/>})
29+
30+
return options;
31+
}
32+
33+
export const getMapUrl = (mapType: MapType, style: MapStyle) => {
34+
switch (mapType) {
35+
case MapType.Maptiler:
36+
return getMapTilerUrl(style);
37+
case MapType.OSM:
38+
return getOSMUrl(style);
39+
default:
40+
alert(`Invalid map type ${mapType}, must be one of: ${Object.values(MapType)}`);
41+
return null;
42+
}
43+
}
44+
45+
export const getMapTilerUrl = (style: MapStyle) => {
46+
if (!MAPTILER_API_KEY) {
47+
alert("MAPTILER_API_KEY must be set to use maptiler");
48+
return null;
49+
}
50+
51+
let mapTilerStyle;
52+
if (style === MapStyle.Light) mapTilerStyle = "streets-v2";
53+
else if (style === MapStyle.Dark) mapTilerStyle = "base-v4-dark";
54+
else if (style === MapStyle.Satellite) mapTilerStyle = "satellite";
55+
else throw new Error(`Unsupported MapTiler style: ${style}`);
56+
57+
58+
return `https://api.maptiler.com/maps/${mapTilerStyle}/style.json?key=${MAPTILER_API_KEY}`;
59+
}
60+
61+
export const getOSMUrl = (style: MapStyle) => {
62+
let source;
63+
if (style === MapStyle.Light) source = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
64+
else if (style === MapStyle.Dark) source = "https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
65+
else throw new Error(`Unsupported OSM style: ${style}`);
66+
67+
return {
68+
version: 8,
69+
sources: {
70+
"osm-tiles": {
71+
type: "raster",
72+
tiles: [source],
73+
tileSize: 256,
74+
attribution: "&copy; OpenStreetMap Contributors"
75+
}
76+
},
77+
layers: [
78+
{
79+
id: "osm-layer",
80+
type: "raster",
81+
source: "osm-tiles",
82+
minzoom: 0,
83+
maxzoom: 19
84+
}
85+
]
86+
};
87+
}

0 commit comments

Comments
 (0)