Skip to content
Closed
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
8 changes: 8 additions & 0 deletions packages/locales/lib/human/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,14 @@
"routes": "Routes",
"route_type": "Route Type",
"routes_subtitle": "View in game routes and relevant information about them on the map",
"routes_from_poi": "{{count}} route{{count === 1 ? '' : 's'}} {{type}} here",
"routes_from_poi_one": "1 route {{type}} here",
"routes_from_poi_other": "{{count}} routes {{type}} here",
"unknown_route": "Unknown Route",
"starting": "starting",
"ending": "ending",
"route_displayed": "Route displayed on map",
"route_click_map_to_hide": "Click anywhere on the map to hide the route",
"description": "Description",
"additional_info": "Additional Info",
"duration": "Duration",
Expand Down
13 changes: 13 additions & 0 deletions src/features/route/EnhancedRouteTile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-check
import * as React from 'react'

import { SimplifiedRouteTile } from './SimplifiedRouteTile'

/**
* Enhanced route tile that supports both old and new route display modes
* @param {import("@rm/types").Route} route
*/
export function EnhancedRouteTile(route) {
// Use the SimplifiedRouteTile approach which works with the individual route processing
return <SimplifiedRouteTile routes={[route]} />
}
146 changes: 146 additions & 0 deletions src/features/route/RouteClusterManager.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// @ts-check
import * as React from 'react'

import { useStorage } from '@store/useStorage'
import { RoutePoiMarker } from './RoutePoiMarker'

/**
* Groups routes by their POI coordinates with proximity tolerance
* For reversible routes, both endpoints are treated as both start AND end points
* @param {import("@rm/types").Route[]} routes
* @param {number} tolerance - Coordinate tolerance for grouping (default: 0.00001)
* @returns {Map<string, { lat: number, lon: number, routes: import("@rm/types").Route[], poiType: 'both' | 'start' | 'end' }>}
*/
function clusterRoutesByPoi(routes, tolerance = 0.00001) {
const clusters = new Map()

routes.forEach((route) => {
// For reversible routes, both endpoints are starting AND ending points
if (route.reversible) {
// Add route to start point cluster (which serves as both start and end)
const startKey = findOrCreateCluster(
clusters,
route.start_lat,
route.start_lon,
tolerance,
'both',
)
clusters.get(startKey).routes.push(route)

// Add route to end point cluster (which serves as both start and end)
const endKey = findOrCreateCluster(
clusters,
route.end_lat,
route.end_lon,
tolerance,
'both',
)
// Only add to end cluster if it's different from start cluster
if (endKey !== startKey) {
clusters.get(endKey).routes.push(route)
}
} else {
// For non-reversible routes, only the start point is a starting point
const startKey = findOrCreateCluster(
clusters,
route.start_lat,
route.start_lon,
tolerance,
'start',
)
clusters.get(startKey).routes.push(route)
}
})

return clusters
}

/**
* Find existing cluster or create new one based on coordinate proximity
*/
function findOrCreateCluster(clusters, lat, lon, tolerance, poiType) {
// Check if there's an existing cluster within tolerance
const existingCluster = Array.from(clusters.entries()).find(([, cluster]) => {
const latDiff = Math.abs(cluster.lat - lat)
const lonDiff = Math.abs(cluster.lon - lon)
return latDiff <= tolerance && lonDiff <= tolerance
})

if (existingCluster) {
return existingCluster[0]
}

// Create new cluster
const key = `${lat.toFixed(6)},${lon.toFixed(6)}`
clusters.set(key, {
lat,
lon,
routes: [],
poiType,
})
return key
}

/**
* Route clustering manager that collects and groups routes
*/
export function RouteClusterManager({ children }) {
const useSimplifiedRoutes = useStorage(
(s) => s.settings?.useSimplifiedRoutes ?? true,
)
const [allRoutes, setAllRoutes] = React.useState([])

// Collect routes from child components
const addRoute = React.useCallback((route) => {
setAllRoutes((prev) => {
// Avoid duplicates
if (prev.find((r) => r.id === route.id)) {
return prev
}
return [...prev, route]
})
}, [])

const removeRoute = React.useCallback((routeId) => {
setAllRoutes((prev) => prev.filter((r) => r.id !== routeId))
}, [])

// Provide context for route collection
const contextValue = React.useMemo(
() => ({
addRoute,
removeRoute,
useSimplifiedRoutes,
}),
[addRoute, removeRoute, useSimplifiedRoutes],
)

// Render clustered routes if simplified mode is enabled
const clusteredRoutes = React.useMemo(() => {
if (!useSimplifiedRoutes || allRoutes.length === 0) {
return []
}
return Array.from(clusterRoutesByPoi(allRoutes).values())
}, [allRoutes, useSimplifiedRoutes])

return (
<RouteClusterContext.Provider value={contextValue}>
{children}

{/* Render clustered POI markers in simplified mode */}
{useSimplifiedRoutes &&
clusteredRoutes.map((cluster) => (
<RoutePoiMarker
key={`${cluster.lat},${cluster.lon}`}
lat={cluster.lat}
lon={cluster.lon}
routes={cluster.routes}
poiType={cluster.poiType}
/>
))}
</RouteClusterContext.Provider>
)
}

// Context for route collection
const RouteClusterContext = React.createContext(null)
107 changes: 107 additions & 0 deletions src/features/route/RoutePoiMarker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// @ts-check
import * as React from 'react'
import { Marker, Polyline } from 'react-leaflet'

import { routeMarker } from './routeMarker'
import { RoutePoiPopup } from './RoutePoiPopup'

/**
* Component for displaying route POI markers with simplified view
* @param {{
* lat: number
* lon: number
* routes: import("@rm/types").Route[]
* poiType: 'start' | 'end'
* }} props
*/
export function RoutePoiMarker({ lat, lon, routes, poiType }) {
const [selectedRoute, setSelectedRoute] = React.useState(null)
const [showRoutes, setShowRoutes] = React.useState(false)

// Use the existing route marker icon from the original implementation
const flagIcon = React.useMemo(() => routeMarker('start'), [])

const handleMarkerClick = React.useCallback(() => {
setShowRoutes(true)
}, [])

const handleRouteSelect = React.useCallback((route) => {
setSelectedRoute(route)
}, [])

const handleRouteDeselect = React.useCallback(() => {
setSelectedRoute(null)
}, [])

const handlePopupClose = React.useCallback(() => {
setShowRoutes(false)
setSelectedRoute(null)
}, [])

// Group routes by their reversibility and type for better organization
const organizedRoutes = React.useMemo(
() =>
routes.map((route) => ({
...route,
waypoints: [
{
lat_degrees: route.start_lat,
lng_degrees: route.start_lon,
elevation_in_meters: route.waypoints[0]?.elevation_in_meters || 0,
},
...route.waypoints,
{
lat_degrees: route.end_lat,
lng_degrees: route.end_lon,
elevation_in_meters:
route.waypoints[route.waypoints.length - 1]
?.elevation_in_meters || 1,
},
],
})),
[routes],
)

return (
<>
<Marker
position={[lat, lon]}
icon={flagIcon}
eventHandlers={{
click: handleMarkerClick,
popupclose: handlePopupClose,
}}
>
<RoutePoiPopup
routes={organizedRoutes}
poiType={poiType}
selectedRoute={selectedRoute}
onRouteSelect={handleRouteSelect}
onRouteDeselect={handleRouteDeselect}
showRoutes={showRoutes}
/>
</Marker>

{/* Render selected route path */}
{selectedRoute && (
<Polyline
positions={selectedRoute.waypoints.map((waypoint) => [
waypoint.lat_degrees,
waypoint.lng_degrees,
])}
pathOptions={{
color: `#${selectedRoute.image_border_color}`,
opacity: 0.8,
weight: 4,
dashArray: selectedRoute.reversible ? undefined : '5, 5',
}}
eventHandlers={{
click: () => {
// Keep the route visible when clicking on it
},
}}
/>
)}
</>
)
}
Loading
Loading