diff --git a/.gitignore b/.gitignore index 31e3542..9944c52 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,7 @@ docs/comparison-*.md # Local dev tools (mock servers, screenshot automation) tools/dev/ + +# PMTiles map data (large binaries, downloaded per-region) +frontend/public/tiles/*.pmtiles fleet-dashboard.png diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 9bcf228..6e3cb6e 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -135,6 +135,8 @@ services: container_name: tod_frontend ports: - "3000:80" + volumes: + - ./frontend/public/tiles:/usr/share/nginx/html/tiles:ro depends_on: - api deploy: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5e1a8ff..1a31174 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "9.0.1", + "version": "9.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "9.0.1", + "version": "9.7.0", "dependencies": { "@dagrejs/dagre": "^2.0.4", "@fontsource-variable/manrope": "^5.2.8", @@ -43,6 +43,7 @@ "framer-motion": "^12.34.3", "leaflet": "^1.9.4", "lucide-react": "^0.575.0", + "protomaps-leaflet": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-leaflet": "^5.0.0", @@ -1416,6 +1417,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", + "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1467,6 +1485,15 @@ "node": ">=18" } }, + "node_modules/@protomaps/basemaps": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@protomaps/basemaps/-/basemaps-5.7.2.tgz", + "integrity": "sha512-K1Yk6bWdULulYg+R2QRVXx4NzJZan5YQhpejEG0c1/sXruJrfPIPZuakpf3jwAgVmjIRVQwAv+yRafDeN0aaUQ==", + "license": "BSD-3-Clause", + "bin": { + "generate_style": "src/cli.ts" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4331,6 +4358,12 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "license": "MIT" + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -4631,7 +4664,6 @@ "version": "1.9.21", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", - "dev": true, "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -4647,6 +4679,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/rbush": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.4.tgz", + "integrity": "sha512-knSt9cCW8jj1ZSFcFeBZaX++OucmfPxxHiRwTahZfJlnQsek7O0bazTJHWD2RVj9LEoejUYF2de3/stf+QXcXw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -5677,6 +5715,12 @@ "dev": true, "license": "MIT" }, + "node_modules/color2k": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", + "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6572,6 +6616,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7652,6 +7702,18 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7735,6 +7797,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pmtiles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-3.2.1.tgz", + "integrity": "sha512-3R4fBwwoli5mw7a6t1IGwOtfmcSAODq6Okz0zkXhS1zi9sz1ssjjIfslwPvcWw5TNhdjNBUg9fgfPLeqZlH6ng==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/leaflet": "^1.9.8", + "fflate": "^0.8.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -7891,6 +7963,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7954,6 +8032,30 @@ "license": "MIT", "peer": true }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, + "node_modules/protomaps-leaflet": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/protomaps-leaflet/-/protomaps-leaflet-5.1.0.tgz", + "integrity": "sha512-N0YY1Gbw5nZ9n8VUOvwHKm/w3zhTTuaZEeMZl8doHemeM4ujGPp2otI6t4Sgqrz8iM4svx2rpG1Q17rHkOWSaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.2", + "@protomaps/basemaps": "^5.0.0", + "@types/css-font-loading-module": "^0.0.7", + "@types/rbush": "^3.0.0", + "color2k": "^2.0.3", + "pbf": "^4.0.1", + "pmtiles": "^3.1.0", + "potpack": "^1.0.2", + "rbush": "^3.0.1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7990,6 +8092,21 @@ ], "license": "MIT" }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -8354,6 +8471,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a44d25a..827f349 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,6 +50,7 @@ "framer-motion": "^12.34.3", "leaflet": "^1.9.4", "lucide-react": "^0.575.0", + "protomaps-leaflet": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-leaflet": "^5.0.0", diff --git a/frontend/src/components/map/FleetMap.tsx b/frontend/src/components/map/FleetMap.tsx index 7287ee6..b6b8202 100644 --- a/frontend/src/components/map/FleetMap.tsx +++ b/frontend/src/components/map/FleetMap.tsx @@ -1,7 +1,8 @@ import { useEffect, useMemo } from 'react' -import { MapContainer, TileLayer, useMap } from 'react-leaflet' +import { MapContainer, useMap } from 'react-leaflet' import MarkerClusterGroup from 'react-leaflet-cluster' import L from 'leaflet' +import * as protomapsL from 'protomaps-leaflet' import type { FleetDevice } from '@/lib/api' import { DeviceMarker } from './DeviceMarker' @@ -16,6 +17,31 @@ interface FleetMapProps { const DEFAULT_CENTER: [number, number] = [20, 0] const DEFAULT_ZOOM = 2 +/** + * Self-hosted PMTiles basemap layer via protomaps-leaflet. + * Loads regional PMTiles files from /tiles/ (no third-party requests). + * Multiple region files are layered — tiles outside downloaded regions show blank. + */ +const PMTILES_REGIONS = ['/tiles/wisconsin.pmtiles', '/tiles/florida.pmtiles'] + +function ProtomapsLayer() { + const map = useMap() + + useEffect(() => { + const layers: L.Layer[] = [] + for (const url of PMTILES_REGIONS) { + const layer = protomapsL.leafletLayer({ url, flavor: 'dark' }) + layer.addTo(map) + layers.push(layer) + } + return () => { + for (const layer of layers) map.removeLayer(layer) + } + }, [map]) + + return null +} + /** * Inner component that auto-fits the map to device bounds * whenever the device list changes. @@ -111,10 +137,7 @@ export function FleetMap({ devices, tenantId }: FleetMapProps) { scrollWheelZoom style={{ background: 'hsl(var(--background))' }} > - +