Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
ac441e0
added responsive dynamic map
Cynamide Mar 27, 2025
86f756d
corrected pathMap and added map bg
Cynamide Mar 27, 2025
4a5b3a8
changed ui a bit
Cynamide Mar 27, 2025
9295bbf
more refactoring
Cynamide Mar 27, 2025
acd3e63
removed mapbox polyline code
Cynamide Mar 27, 2025
3a70985
added routemap heading
Cynamide Mar 27, 2025
d12c93d
fixed route statistics
Cynamide Mar 27, 2025
3ed8263
fixed linting using biome
Cynamide Mar 27, 2025
d864806
added smooth marker transitions
Cynamide Mar 29, 2025
6da1115
update
Cynamide Mar 29, 2025
45f34dc
linting
Cynamide Mar 29, 2025
7c0934c
fix lockfile
incognitojam Mar 29, 2025
3baa567
remove @mapbox/polyline
incognitojam Mar 29, 2025
605a19d
fixed marker anim when updateTime and map now reloads when new route …
Cynamide Mar 30, 2025
fd3937f
Merge branch 'interactive-map' of https://github.com/Cynamide/new-con…
Cynamide Mar 30, 2025
f284283
Merge branch 'commaai:master' into interactive-map
Cynamide Mar 30, 2025
da1f255
added lock/unlock button, zoom and smaller marker. removed gestures(e…
Cynamide Mar 31, 2025
a2d7b7c
Merge branch 'interactive-map' of https://github.com/Cynamide/new-con…
Cynamide Mar 31, 2025
ece31fb
Merge branch 'commaai:master' into interactive-map
Cynamide Mar 31, 2025
570c926
added popup for map usage and some refactoring
Cynamide Mar 31, 2025
b66ce29
fixed grey map bug on reload
Cynamide Mar 31, 2025
4aeb3a1
fixed marker transition on map lock
Cynamide Mar 31, 2025
aed4e48
fix: Increase car marker icon
TheSecurityDev Mar 31, 2025
7d93d76
feat: Increase route path width to 5
TheSecurityDev Mar 31, 2025
3376874
fix: Re-enable zoom via scroll and refactor how map is enabled/disabled
TheSecurityDev Mar 31, 2025
23b46ee
refactor: Rearrange imports
TheSecurityDev Mar 31, 2025
c7e885e
removed help text added panning while seeking/dragging, fixed marker …
Cynamide Mar 31, 2025
ab42071
Merge branch 'master' into interactive-map
Cynamide Mar 31, 2025
535c307
typo
Cynamide Mar 31, 2025
3e01623
Merge branch 'master' into interactive-map
Cynamide Mar 31, 2025
a7b5fff
Merge remote-tracking branch 'Cynamide/interactive-map' into cynamide…
TheSecurityDev Mar 31, 2025
2a7510f
fix: Fix dragging
TheSecurityDev Mar 31, 2025
4cbb91a
fix: Merge timeline adjustment with master
TheSecurityDev Mar 31, 2025
484ae7d
fix: Merge in RouteVideoPlayer update to fix video playing
TheSecurityDev Mar 31, 2025
0a58321
Merge remote-tracking branch 'Comma/master' into cynamide-interactive…
TheSecurityDev Mar 31, 2025
2c65ffd
add static map file
Cynamide Mar 31, 2025
91a2442
Merge branch 'master' into interactive-map
Cynamide Mar 31, 2025
3aa7891
linting
Cynamide Mar 31, 2025
7bc12b3
Merge remote-tracking branch 'Commaai/master' into interactive-map
Cynamide Mar 31, 2025
961abee
Merge remote-tracking branch 'Comma/master' into cynamide-interactive…
TheSecurityDev Mar 31, 2025
8a4958e
fixed import
Cynamide Mar 31, 2025
fb551d3
Merge remote-tracking branch 'Cynamide/interactive-map' into cynamide…
TheSecurityDev Mar 31, 2025
c1c4c96
fix: unlocked icon color
TheSecurityDev Apr 1, 2025
308fc30
feat: set map zoom to fit route bounds on initialization
TheSecurityDev Apr 1, 2025
2d27921
refactor: Rename PathMap to RoutePathMap with default export
TheSecurityDev Apr 1, 2025
5a263a8
Merge remote-tracking branch 'Comma/master' into cynamide-interactive…
TheSecurityDev Apr 1, 2025
eb5856a
refactor: Remove unnecessary For component and routeName prop from Ro…
TheSecurityDev Apr 1, 2025
13f7594
fix: Don't center marker when unlock pressed
TheSecurityDev Apr 1, 2025
a838175
refactor: swap past/future colors, add opacity, remove unused color prop
TheSecurityDev Apr 1, 2025
b296a50
fix: revert swap colors
TheSecurityDev Apr 1, 2025
79eaf90
refactor: add showTransition signal to manage marker class
TheSecurityDev Apr 1, 2025
b98eec4
fix: Prevent pan animation on first load to fix initial position; add…
TheSecurityDev Apr 1, 2025
81466bd
refactor: Remove isMapInteractive and just use isLocked
TheSecurityDev Apr 1, 2025
cabce1d
feat: Toggle locked state when marker clicked
TheSecurityDev Apr 1, 2025
c4cb77c
feat: Center marker after dragging
TheSecurityDev Apr 1, 2025
a8b6f87
fix: Fix drag end
TheSecurityDev Apr 1, 2025
e1512d6
fix: Fix drag end and center marker on route point selected; refactor…
TheSecurityDev Apr 1, 2025
2dc27fe
refactor: Round seek time and skip if not changed to reduce number of…
TheSecurityDev Apr 1, 2025
469c707
fix: Fix icon hitbox size
TheSecurityDev Apr 1, 2025
8ade90c
feat: Change marker icon foreground color when not locked
TheSecurityDev Apr 1, 2025
7e84890
Merge remote-tracking branch 'Comma/master' into cynamide-interactive…
TheSecurityDev Apr 1, 2025
73a1b8b
refactor: cleanup props
TheSecurityDev Apr 1, 2025
73e4c17
fix: update is dragging to false correctly
TheSecurityDev Apr 1, 2025
5f77c3b
feat: don't unlock on drag
TheSecurityDev Apr 1, 2025
619af69
fix: just don't transition during dragging instead of explicitly sett…
TheSecurityDev Apr 2, 2025
52c9f97
Merge remote-tracking branch 'Comma/master' into cynamide-interactive…
TheSecurityDev Apr 2, 2025
a5cce05
Merge remote-tracking branch 'Comma/master' into cynamide-interactive…
TheSecurityDev Apr 2, 2025
a881287
fix: Update hitbox and fit to bounds when route changes since mount i…
TheSecurityDev Apr 2, 2025
bae5828
fix: Remove debug hitbox line color
TheSecurityDev Apr 2, 2025
9d8c40b
fix: Handle undefined current coordinate by providing a default value
TheSecurityDev Apr 2, 2025
4991f42
Merge remote-tracking branch 'Comma/master' into cynamide-interactive…
TheSecurityDev Apr 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
844 changes: 165 additions & 679 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"type": "module",
"//devDependencies": {
"@solidjs/testing-library": "test solid components (only used for App.jsx component)",
"@types/mapbox__polyline": "types for mapbox elements",
"@vite-pwa/assets-generator": "generate PWA assets during build",
"@vitest/browser": "run vitest suite in browser using playwright",
"autoprefixer": "adds css vendor prefixes automatically",
Expand All @@ -47,7 +46,6 @@
"@types/bun": "^1.2.5",
"@types/geojson": "^7946.0.16",
"@types/leaflet": "^1.9.16",
"@types/mapbox__polyline": "^1.0.5",
"@vite-pwa/assets-generator": "^1.0.0",
"@vitest/browser": "^3.0.9",
"autoprefixer": "^10.4.21",
Expand All @@ -65,7 +63,6 @@
"wrangler": "^4.3.0"
},
"//dependencies": {
"@mapbox/polyline": "display series of map coordinates as a line",
"@sentry/solid": "error monitoring",
"@solidjs/router": "changes view based on browser url",
"clsx": "construct className strings conditionally",
Expand All @@ -75,7 +72,6 @@
"solid-js": "JS ui framework"
},
"dependencies": {
"@mapbox/polyline": "^1.2.1",
"@sentry/solid": "^9.8.0",
"@sentry/vite-plugin": "^3.2.2",
"@solid-primitives/state-machine": "^0.1.0",
Expand Down
57 changes: 57 additions & 0 deletions src/components/RouteDynamicMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { createResource, Match, Switch } from 'solid-js'
import type { Accessor, JSXElement, VoidComponent } from 'solid-js'
import clsx from 'clsx'

import { getCoords } from '~/api/derived'
import { getThemeId } from '~/theme'
import type { Route } from '~/api/types'

import Icon from '~/components/material/Icon'
import RoutePathMap from '~/components/RoutePathMap'

const State = (props: {
children: JSXElement
trailing?: JSXElement
opaque?: boolean
}) => {
return (
<div class={clsx('absolute flex size-full items-center justify-center gap-2', props.opaque && 'bg-surface text-on-surface')}>
<span class="text-label-sm">{props.children}</span>
{props.trailing}
</div>
)
}

type RouteDynamicMapProps = {
class?: string
route: Route | undefined
seekTime: Accessor<number>
updateTime: (newTime: number) => void
}

const RouteDynamicMap: VoidComponent<RouteDynamicMapProps> = (props) => {
const [coords] = createResource(() => props.route, getCoords)
const themeId = getThemeId()

return (
<div class={clsx('relative isolate flex h-full flex-col justify-end self-stretch bg-surface text-on-surface', props.class)}>
<Switch>
<Match when={coords() === undefined || coords()?.length === 0} keyed>
<State trailing={<Icon name="satellite_alt" filled />}>No GPS data</State>
</Match>
<Match when={(coords()?.length ?? 0) > 0} keyed>
<RoutePathMap
themeId={themeId}
seekTime={props.seekTime}
updateTime={props.updateTime}
coords={coords()!}
strokeWidth={5}
opacity={0.8}
/>
</Match>
</Switch>
</div>
)
}

export default RouteDynamicMap
228 changes: 228 additions & 0 deletions src/components/RoutePathMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { Accessor, Component, createEffect, createSignal, onMount, onCleanup } from 'solid-js'
import { render } from 'solid-js/web'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'

import { getTileUrl } from '~/map'
import { GPSPathPoint } from '~/api/derived'

import IconButton from './material/IconButton'
import Icon from './material/Icon'

const findClosestPoint = (lng: number, lat: number, coords: GPSPathPoint[]): number =>
coords.reduce(
(closest, point, i) => {
const dist = Math.sqrt((point.lng - lng) ** 2 + (point.lat - lat) ** 2)
return dist < closest.minDist ? { minDist: dist, index: i } : closest
},
{ minDist: Infinity, index: 0 },
).index

const createCarIcon = (locked: boolean) => {
const el = document.createElement('div')
render(
() => (
<div
class={`flex size-[30px] items-center justify-center rounded-full bg-primary-container ${locked ? 'text-on-primary-container' : 'text-on-surface-variant'}`}
>
<Icon size="20" name="directions_car" />
</div>
),
el,
)
return L.divIcon({ className: 'car-icon', html: el.innerHTML, iconSize: [30, 30], iconAnchor: [15, 15] })
}

const RoutePathMap: Component<{
themeId: string
seekTime: Accessor<number>
updateTime: (newTime: number) => void
coords: GPSPathPoint[]
strokeWidth?: number
opacity?: number
}> = (props) => {
let mapRef!: HTMLDivElement
const [map, setMap] = createSignal<L.Map | null>(null)
const [position, setPosition] = createSignal(0) // current position in the route
const [isLocked, setIsLocked] = createSignal(true) // auto track and center with map interaction disabled
const [isDragging, setIsDragging] = createSignal(false) // marker is being dragged
const [showTransition, setShowTransition] = createSignal(false) // smooth marker transition

const mapCoords = () => props.coords.map((p) => [p.lat, p.lng] as [number, number])
const pastCoords = () => mapCoords().slice(0, position() + 1)
const futureCoords = () => mapCoords().slice(position())
const currentCoord = () => mapCoords()[position()] ?? [0, 0]

let lastSeekTime = 0
let marker: L.Marker | null = null
let pastPolyline: L.Polyline | null = null
let futurePolyline: L.Polyline | null = null
let hitboxPolyline: L.Polyline | null = null

const centerMarker: (animateDuration?: number) => void = (animateDuration = 0.25) => {
map()?.panTo(currentCoord(), { animate: animateDuration > 0, duration: animateDuration })
}

onMount(() => {
setTimeout(() => {
window.dispatchEvent(new Event('resize')) // The map size is updated after the first load, so redraw it after a delay
}, 1000)

// Create the Leaflet map
const m = L.map(mapRef, {
zoomControl: true,
attributionControl: false,
dragging: false,
touchZoom: false,
doubleClickZoom: false,
scrollWheelZoom: false,
boxZoom: false,
})
L.tileLayer(getTileUrl()).addTo(m)
m.setView(currentCoord(), 12) // initialize with default zoom (fit to bounds later)
m.zoomControl.setPosition('topright')
const { strokeWidth = 4, opacity } = props
pastPolyline = L.polyline([], { color: '#6F707F', weight: strokeWidth, opacity }).addTo(m)
futurePolyline = L.polyline([], { color: '#DFE0FF', weight: strokeWidth, opacity }).addTo(m)
hitboxPolyline = L.polyline(mapCoords(), { color: 'transparent', weight: (strokeWidth || 4) + 16, opacity: 0 }).addTo(m)
marker = L.marker(currentCoord(), { icon: createCarIcon(isLocked()), draggable: true }).addTo(m)

const updatePosition = (lng: number, lat: number) => {
const idx = findClosestPoint(lng, lat, props.coords)
const point = mapCoords()[idx]
marker?.setLatLng(point)
setPosition(idx)
props.updateTime(props.coords[idx].t)
}

const handleDrag = (e: L.LeafletMouseEvent | L.LeafletEvent) => {
setIsDragging(true)
const { lng, lat } = 'latlng' in e ? e.latlng : e.target.getLatLng()
updatePosition(lng, lat)
}

const handleDragEnd = () => {
setIsDragging(false)
centerMarker() // Center marker on map when dragging ends
}

marker.on('click', () => {
setIsLocked(!isLocked()) // Toggle lock state when marker clicked
centerMarker() // Center marker when it's clicked
})
marker.on('drag', handleDrag).on('dragend', handleDragEnd)
hitboxPolyline?.on('mousedown', (e) => {
handleDrag(e)
centerMarker() // Center marker when just selecting route point without dragging
})
hitboxPolyline?.on('mouseup', handleDragEnd)
m?.on('mouseup', () => setIsDragging(false)) // this is needed for some cases, but we don't want to center the marker

setMap(m)
onCleanup(() => m.remove())
})

// Update hitbox polyline when route changes and fit to bounds
createEffect(() => {
if (!hitboxPolyline) return
hitboxPolyline.setLatLngs(mapCoords())
map()?.fitBounds(hitboxPolyline.getBounds(), { padding: [20, 20] }) // Set initial view so route is fully visible
})

// Update map interactivity
createEffect(() => {
const m = map()
if (!m) return
if (isLocked()) {
m.dragging.disable()
m.touchZoom.disable()
m.doubleClickZoom.disable()
m.scrollWheelZoom.disable()
m.boxZoom.disable()
} else {
m.dragging.enable()
m.touchZoom.enable()
m.doubleClickZoom.enable()
m.scrollWheelZoom.enable()
m.boxZoom.enable()
}
})

// Update marker position based on seek time
createEffect(() => {
const t = Math.round(props.seekTime())
const delta = t - lastSeekTime
// Don't animate if not smoothly seeking forward or for the first pan (to fix initial load position)
setShowTransition(lastSeekTime > 0 && delta >= 0 && delta <= 1)
if (t === lastSeekTime) return // Skip if seek time hasn't changed, since it will just get the same position
lastSeekTime = t
if (!props.coords.length) return
if (t < props.coords[0].t) {
setPosition(0)
return
}
const newPos = props.coords.findIndex((p, i) => i === props.coords.length - 1 || (t >= p.t && t < props.coords[i + 1].t))
setPosition(newPos === -1 ? props.coords.length - 1 : newPos)
})

// Update polyline and marker position based on coordinates, and auto center if locked
createEffect(() => {
if (!map() || !props.coords.length) return

pastPolyline?.setLatLngs(pastCoords())
futurePolyline?.setLatLngs(futureCoords())
marker?.setLatLng(currentCoord())

if (isLocked() && !isDragging()) centerMarker(showTransition() ? 2 : 0.25)
})

// Update marker animation class
createEffect(() => {
const markerClassList = marker?.getElement()?.classList
if (showTransition() && !isDragging()) markerClassList?.remove('no-transition')
else markerClassList?.add('no-transition')
})

// Update marker icon
createEffect(() => {
marker?.setIcon(createCarIcon(isLocked()))
})

return (
<div ref={mapRef} class="h-full relative" style={{ 'background-color': 'rgb(19 19 24)' }}>
<style>
{`
.leaflet-bar a {
background-color: rgb(83 90 146) !important;
color: rgb(255 255 255) !important;
}
.leaflet-marker-pane > * {
-webkit-transition: transform 1.2s linear;
-moz-transition: transform 1.2s linear;
-o-transition: transform 1.2s linear;
-ms-transition: transform 1.2s linear;
transition: transform 1.2s linear;
}
.leaflet-marker-pane > .no-transition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}
`}
</style>
<IconButton
name={isLocked() ? 'my_location' : 'location_searching'}
class={`absolute z-[1000] left-4 top-4 bg-surface-variant ${isLocked() ? 'text-primary' : 'text-white'}`}
onClick={() => {
const newLocked = !isLocked()
setIsLocked(newLocked)
if (newLocked) centerMarker()
}}
/>
</div>
)
}

export default RoutePathMap
76 changes: 0 additions & 76 deletions src/components/RouteStaticMap.tsx

This file was deleted.

Loading
Loading