Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"i18next-http-backend": "2.5.2",
"knex": "3.1.0",
"leaflet": "1.9.4",
"leaflet-arrowheads": "^1.4.0",
"leaflet.locatecontrol": "0.81.0",
"lodash": "^4.17.21",
"moment-timezone": "^0.5.43",
Expand Down
25 changes: 25 additions & 0 deletions src/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ body {
background-color: #ff4b4d;
}

.route-count-wrapper {
display: inline-flex;
transform: translate(10px, -12px);
}

.route-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
padding: 0 4px;
border-radius: 10px;
background: #ff4b4d;
color: #fff;
font-size: 11px;
font-weight: 700;
line-height: 16px;
border: 1px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
}

.route-count-badge--destination {
background: #2196f3;
}

.invasion-exists {
border: 4px solid rgb(141, 13, 13);
}
Expand Down
19 changes: 19 additions & 0 deletions src/features/drawer/Routes.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// @ts-check
import * as React from 'react'
import ListItem from '@mui/material/ListItem'
import ListItemText from '@mui/material/ListItemText'
import Switch from '@mui/material/Switch'
import { useTranslation } from 'react-i18next'

import { useMemory } from '@store/useMemory'
import { useStorage, useDeepStore } from '@store/useStorage'
Expand All @@ -9,8 +12,13 @@ import { SliderTile } from '@components/inputs/SliderTile'
import { CollapsibleItem } from './components/CollapsibleItem'

const RouteSlider = () => {
const { t } = useTranslation()
const enabled = useStorage((s) => !!s.filters?.routes?.enabled)
const [filters, setFilters] = useDeepStore('filters.routes.distance')
const [compactView, setCompactView] = useDeepStore(
'userSettings.routes.compactView',
true,
)
const baseDistance = useMemory.getState().filters?.routes?.distance

/** @type {import('@rm/types').RMSlider} */
Expand All @@ -31,6 +39,17 @@ const RouteSlider = () => {

return (
<CollapsibleItem open={enabled}>
<ListItem
secondaryAction={
<Switch
color="secondary"
onChange={(_, checked) => setCompactView(checked)}
checked={compactView !== false}
/>
}
>
<ListItemText primary={t('compact_route_view', 'Compact Route View')} />
</ListItem>
<ListItem>
<SliderTile
slide={slider}
Expand Down
10 changes: 10 additions & 0 deletions src/features/gym/GymTile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useForcePopup } from '@hooks/useForcePopup'
import { sendNotification } from '@services/desktopNotification'
import { TooltipWrapper } from '@components/ToolTipWrapper'
import { getTimeUntil } from '@utils/getTimeUntil'
import { useRouteStore } from '@features/route'

import { gymMarker } from './gymMarker'
import { GymPopup } from './GymPopup'
Expand All @@ -38,6 +39,8 @@ const getColor = (team) => {
const BaseGymTile = (gym) => {
const [markerRef, setMarkerRef] = React.useState(null)
const [stateChange, setStateChange] = React.useState(false)
const hasRoutes = useRouteStore((s) => !!s.poiIndex[gym.id])
const selectPoi = useRouteStore((s) => s.selectPoi)

const [
hasRaid,
Expand Down Expand Up @@ -179,6 +182,13 @@ const BaseGymTile = (gym) => {
raidIconSize,
...gym,
})}
eventHandlers={{
click: () => {
if (hasRoutes) {
selectPoi(gym.id)
}
},
}}
>
<Popup position={[gym.lat, gym.lon]}>
<GymPopup
Expand Down
10 changes: 10 additions & 0 deletions src/features/pokestop/PokestopTile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Marker, Popup, Circle } from 'react-leaflet'
import { useMarkerTimer } from '@hooks/useMarkerTimer'
import { basicEqualFn, useMemory } from '@store/useMemory'
import { useStorage } from '@store/useStorage'
import { useRouteStore } from '@features/route'
import { useForcePopup } from '@hooks/useForcePopup'
import { TooltipWrapper } from '@components/ToolTipWrapper'

Expand All @@ -20,6 +21,8 @@ import { usePokestopMarker } from './usePokestopMarker'
const BasePokestopTile = (pokestop) => {
const [stateChange, setStateChange] = React.useState(false)
const [markerRef, setMarkerRef] = React.useState(null)
const hasRoutes = useRouteStore((s) => !!s.poiIndex[pokestop.id])
const selectPoi = useRouteStore((s) => s.selectPoi)

const [
hasLure,
Expand Down Expand Up @@ -130,6 +133,13 @@ const BasePokestopTile = (pokestop) => {
ref={setMarkerRef}
position={[pokestop.lat, pokestop.lon]}
icon={icon}
eventHandlers={{
click: () => {
if (hasRoutes) {
selectPoi(pokestop.id)
}
},
}}
>
<Popup position={[pokestop.lat, pokestop.lon]}>
<PokestopPopup
Expand Down
222 changes: 222 additions & 0 deletions src/features/route/RouteLayer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// @ts-check
import * as React from 'react'
import { Marker, useMapEvents } from 'react-leaflet'
import { divIcon } from 'leaflet'

import { useStorage } from '@store/useStorage'

import { RouteTile } from './RouteTile'
import { routeMarker } from './routeMarker'
import {
useRouteStore,
ROUTE_COORD_EPSILON,
getRouteCoordKey,
} from './useRouteStore'

const ACTIVE_Z_INDEX = 1800
const INACTIVE_Z_INDEX = 900

const RouteAnchor = React.memo(
({
entry,
selected,
onSelect,
routeCount,
variant = 'start',
icon = null,
}) => {
const baseIcon = React.useMemo(
() => icon || routeMarker(variant === 'destination' ? 'end' : 'start'),
[icon, variant],
)
const badgeIcon = React.useMemo(() => {
if (routeCount <= 1) return null
return divIcon({
className: 'route-count-wrapper',
html: `<span class="route-count-badge route-count-badge--${variant}">${routeCount}</span>`,
iconSize: [0, 0],
iconAnchor: [0, 0],
})
}, [routeCount, variant])

return (
<>
{variant !== 'destination' && !selected && (
<Marker
position={[entry.lat, entry.lon]}
icon={baseIcon}
zIndexOffset={INACTIVE_Z_INDEX}
riseOnHover
eventHandlers={{
click: () => onSelect(entry.key),
}}
title={routeCount > 1 ? `${routeCount} routes` : ''}
/>
)}
{badgeIcon && (
<Marker
position={[entry.lat, entry.lon]}
icon={badgeIcon}
interactive={false}
keyboard={false}
pane="tooltipPane"
zIndexOffset={
selected ? ACTIVE_Z_INDEX + 200 : INACTIVE_Z_INDEX + 200
}
/>
)}
</>
)
},
)

const ActiveRoute = React.memo(({ selection }) => {
const route = useRouteStore(
React.useCallback(
(state) => state.routeCache[selection.routeId],
[selection.routeId],
),
)

if (!route) return null
return <RouteTile route={route} orientation={selection.orientation} />
})

export function RouteLayer({ routes }) {
const enabled = useStorage((s) => !!s.filters?.routes?.enabled)
const compactView = useStorage(
(s) => s.userSettings.routes?.compactView ?? true,
)
const syncRoutes = useRouteStore((s) => s.syncRoutes)
const poiIndex = useRouteStore((s) => s.poiIndex)
const routeCache = useRouteStore((s) => s.routeCache)
const activeRoutes = useRouteStore((s) => s.activeRoutes)
const activePoiId = useRouteStore((s) => s.activePoiId)
const selectPoi = useRouteStore((s) => s.selectPoi)
const clearSelection = useRouteStore((s) => s.clearSelection)

React.useEffect(() => {
syncRoutes(routes || [])
}, [routes, syncRoutes])

React.useEffect(() => {
if (!enabled || !compactView) {
clearSelection()
}
}, [enabled, compactView, clearSelection])

useMapEvents({
click: ({ originalEvent }) => {
if (!originalEvent.defaultPrevented) {
clearSelection()
}
},
})

const destinationSummary = React.useMemo(() => {
if (!compactView)
return { keys: new Set(), icons: new Map(), counts: new Map() }
const keys = new Set()
const icons = new Map()
const counts = new Map()
activeRoutes.forEach((selection) => {
const route = routeCache[selection.routeId]
if (!route) return
const isForward = selection.orientation === 'forward'
const lat = isForward ? route.end_lat : route.start_lat
const lon = isForward ? route.end_lon : route.start_lon
const coordKey = getRouteCoordKey(lat, lon)
keys.add(coordKey)
counts.set(coordKey, (counts.get(coordKey) || 0) + 1)
if (!icons.has(coordKey)) {
icons.set(coordKey, routeMarker('end'))
}
})
return { keys, icons, counts }
}, [activeRoutes, routeCache, compactView])

const anchors = React.useMemo(() => {
if (!compactView) return []
const values = Object.values(poiIndex)
return values.map((entry) => {
const uniqueRoutes = new Set()
values.forEach((candidate) => {
if (
Math.abs(candidate.lat - entry.lat) <= ROUTE_COORD_EPSILON &&
Math.abs(candidate.lon - entry.lon) <= ROUTE_COORD_EPSILON
) {
candidate.routes.forEach((ref) => {
if (routeCache[ref.routeId]) {
uniqueRoutes.add(ref.routeId)
}
})
}
})
return {
entry,
routeCount:
uniqueRoutes.size || new Set(entry.routes.map((r) => r.routeId)).size,
}
})
}, [compactView, poiIndex, routeCache])

if (!enabled) {
return null
}

if (!compactView) {
return (
<>
{routes.map((route) => (
<RouteTile key={route.id} route={route} />
))}
</>
)
}

return (
<>
{anchors.map(({ entry, routeCount }) => {
const entryCoordKey = getRouteCoordKey(entry.lat, entry.lon)
const iconOverride = destinationSummary.icons.get(entryCoordKey)
const destinationCount = destinationSummary.counts.get(entryCoordKey)
if (destinationCount && destinationCount > 1) {
return (
<RouteAnchor
key={`${entry.key}-destination`}
entry={entry}
selected={entry.key === activePoiId}
onSelect={selectPoi}
routeCount={destinationCount}
variant="destination"
icon={iconOverride || undefined}
/>
)
}
if (
destinationSummary.keys.has(entryCoordKey) &&
entry.key !== activePoiId
) {
return null
}
return (
<RouteAnchor
key={entry.key}
entry={entry}
selected={entry.key === activePoiId}
onSelect={selectPoi}
routeCount={destinationCount || routeCount}
variant="start"
icon={iconOverride || undefined}
/>
)
})}
{activeRoutes.map((selection) => (
<ActiveRoute
key={`${selection.routeId}-${selection.orientation}`}
selection={selection}
/>
))}
</>
)
}
Loading
Loading