diff --git a/package.json b/package.json index c5c4609bf..56269d525 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/assets/css/main.css b/src/assets/css/main.css index e231a06eb..72c7badcd 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -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); } diff --git a/src/features/drawer/Routes.jsx b/src/features/drawer/Routes.jsx index 008238457..b7cb322ae 100644 --- a/src/features/drawer/Routes.jsx +++ b/src/features/drawer/Routes.jsx @@ -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' @@ -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} */ @@ -31,6 +39,17 @@ const RouteSlider = () => { return ( + setCompactView(checked)} + checked={compactView !== false} + /> + } + > + + { 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, @@ -179,6 +182,13 @@ const BaseGymTile = (gym) => { raidIconSize, ...gym, })} + eventHandlers={{ + click: () => { + if (hasRoutes) { + selectPoi(gym.id) + } + }, + }} > { 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, @@ -130,6 +133,13 @@ const BasePokestopTile = (pokestop) => { ref={setMarkerRef} position={[pokestop.lat, pokestop.lon]} icon={icon} + eventHandlers={{ + click: () => { + if (hasRoutes) { + selectPoi(pokestop.id) + } + }, + }} > { + 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: `${routeCount}`, + iconSize: [0, 0], + iconAnchor: [0, 0], + }) + }, [routeCount, variant]) + + return ( + <> + {variant !== 'destination' && !selected && ( + onSelect(entry.key), + }} + title={routeCount > 1 ? `${routeCount} routes` : ''} + /> + )} + {badgeIcon && ( + + )} + + ) + }, +) + +const ActiveRoute = React.memo(({ selection }) => { + const route = useRouteStore( + React.useCallback( + (state) => state.routeCache[selection.routeId], + [selection.routeId], + ), + ) + + if (!route) return null + return +}) + +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) => ( + + ))} + + ) + } + + 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 ( + + ) + } + if ( + destinationSummary.keys.has(entryCoordKey) && + entry.key !== activePoiId + ) { + return null + } + return ( + + ) + })} + {activeRoutes.map((selection) => ( + + ))} + + ) +} diff --git a/src/features/route/RoutePopup.jsx b/src/features/route/RoutePopup.jsx index 5200fb46c..c88732a48 100644 --- a/src/features/route/RoutePopup.jsx +++ b/src/features/route/RoutePopup.jsx @@ -131,7 +131,7 @@ function ExpandableWrapper({ disabled = false, children, expandKey, primary }) { * @param {import("@rm/types").Route & { end?: boolean }} props * @returns */ -export function RoutePopup({ end, ...props }) { +export function RoutePopup({ end, inline = false, ...props }) { const [route, setRoute] = React.useState({ ...props, tags: [] }) const { config } = useMemory.getState() const formatDistance = useFormatDistance() @@ -146,6 +146,12 @@ export function RoutePopup({ end, ...props }) { }) const { t } = useTranslation() + React.useEffect(() => { + if (inline && !called) { + getRoute() + } + }, [inline, called, getRoute]) + React.useEffect(() => { if (data?.route) { setRoute({ @@ -206,14 +212,8 @@ export function RoutePopup({ end, ...props }) { } }, [route.shortcode, t]) - return ( - { - if (ref && ref.isOpen() && !called) { - getRoute() - } - }} - > + const content = ( + <> {notification.message} + + ) + + if (inline) { + return content + } + + return ( + { + if (ref && ref.isOpen() && !called) { + getRoute() + } + }} + > + {content} ) } diff --git a/src/features/route/RouteTile.jsx b/src/features/route/RouteTile.jsx index c54baf6c4..aa1d09350 100644 --- a/src/features/route/RouteTile.jsx +++ b/src/features/route/RouteTile.jsx @@ -1,7 +1,8 @@ /* eslint-disable react/destructuring-assignment */ // @ts-check import * as React from 'react' -import { Marker, Polyline, useMapEvents } from 'react-leaflet' +import { Marker, Polyline, Popup, useMapEvents } from 'react-leaflet' +import 'leaflet-arrowheads' import { darken } from '@mui/material/styles' import { useForcePopup } from '@hooks/useForcePopup' @@ -14,43 +15,83 @@ const POSITIONS = /** @type {const} */ (['start', 'end']) const LINE_OPACITY = 0.33 const MARKER_OPACITY = LINE_OPACITY * 2 -/** - * - * @param {import("@rm/types").Route} route - * @returns - */ -const BaseRouteTile = (route) => { +const BaseRouteTile = ({ route, orientation = 'forward' }) => { const [clicked, setClicked] = React.useState(false) const [hover, setHover] = React.useState('') + const [linePopup, setLinePopup] = React.useState( + /** @type {import('leaflet').LatLngExpression | null} */ (null), + ) /** @type {React.MutableRefObject} */ const lineRef = React.useRef() const [markerRef, setMarkerRef] = React.useState(null) + /** @type {React.MutableRefObject} */ + const arrowheadsRef = React.useRef(null) - const waypoints = React.useMemo( - () => [ + const displayRoute = React.useMemo(() => { + if (orientation === 'forward') return route + const reversedWaypoints = [...(route.waypoints || [])] + .map((waypoint) => ({ ...waypoint })) + .reverse() + return { + ...route, + start_lat: route.end_lat, + start_lon: route.end_lon, + start_image: route.end_image, + start_fort_id: route.end_fort_id, + end_lat: route.start_lat, + end_lon: route.start_lon, + end_image: route.start_image, + end_fort_id: route.start_fort_id, + waypoints: reversedWaypoints, + } + }, [orientation, route]) + + const waypoints = React.useMemo(() => { + const internal = displayRoute.waypoints || [] + return [ { - lat_degrees: route.start_lat, - lng_degrees: route.start_lon, - elevation_in_meters: route.waypoints[0]?.elevation_in_meters || 0, + lat_degrees: displayRoute.start_lat, + lng_degrees: displayRoute.start_lon, + elevation_in_meters: internal[0]?.elevation_in_meters || 0, }, - ...route.waypoints, + ...internal, { - lat_degrees: route.end_lat, - lng_degrees: route.end_lon, + lat_degrees: displayRoute.end_lat, + lng_degrees: displayRoute.end_lon, elevation_in_meters: - route.waypoints[route.waypoints.length - 1]?.elevation_in_meters || 1, + internal[internal.length - 1]?.elevation_in_meters || 1, }, - ], - [route], - ) + ] + }, [displayRoute]) const [color, darkened] = React.useMemo( () => [ - `#${route.image_border_color}`, - darken(`#${route.image_border_color}`, 0.2), + `#${displayRoute.image_border_color}`, + darken(`#${displayRoute.image_border_color}`, 0.2), ], - [route.image_border_color], + [displayRoute.image_border_color], + ) + + const polylinePositions = React.useMemo( + () => + waypoints.map((waypoint) => [waypoint.lat_degrees, waypoint.lng_degrees]), + [waypoints], + ) + + const applyArrowheadStyle = React.useCallback( + (targetColor, targetOpacity) => { + const group = arrowheadsRef.current + if (!group) { + return + } + /** @type {any} */ group.eachLayer((layer) => { + if (layer && typeof layer.setStyle === 'function') { + layer.setStyle({ color: targetColor, opacity: targetOpacity }) + } + }) + }, + [], ) useMapEvents({ @@ -58,10 +99,80 @@ const BaseRouteTile = (route) => { if (!originalEvent.defaultPrevented) { setClicked(false) setHover('') + setLinePopup(null) } }, }) - useForcePopup(route.id, markerRef) + useForcePopup(displayRoute.id, markerRef) + + React.useEffect(() => { + setLinePopup(null) + }, [displayRoute.id, orientation]) + + React.useEffect(() => { + const line = lineRef.current + if (!line) { + arrowheadsRef.current = null + return undefined + } + + const arrowLine = /** @type {any} */ (line) + if (typeof arrowLine.deleteArrowheads === 'function') { + arrowLine.deleteArrowheads() + } + arrowheadsRef.current = null + + if ( + !displayRoute.reversible && + typeof arrowLine.arrowheads === 'function' + ) { + arrowLine.arrowheads({ + size: '10px', + frequency: '24px', + yawn: 32, + fill: false, + offsets: { + start: '10px', + end: '10px', + }, + }) + if (typeof line.redraw === 'function') { + line.redraw() + } + if (typeof arrowLine.getArrowheads === 'function') { + try { + const group = arrowLine.getArrowheads() + arrowheadsRef.current = group || null + } catch (error) { + arrowheadsRef.current = null + } + } + applyArrowheadStyle(color, LINE_OPACITY) + } + + return () => { + if (typeof arrowLine.deleteArrowheads === 'function') { + arrowLine.deleteArrowheads() + } + arrowheadsRef.current = null + } + }, [applyArrowheadStyle, color, displayRoute.reversible, polylinePositions]) + + const isActive = Boolean(clicked || hover) + + React.useEffect(() => { + if (lineRef.current) { + const lineOpacity = isActive ? 1 : LINE_OPACITY + lineRef.current.setStyle({ + color: isActive ? darkened : color, + opacity: lineOpacity, + }) + } + applyArrowheadStyle( + isActive ? darkened : color, + isActive ? 1 : LINE_OPACITY, + ) + }, [applyArrowheadStyle, color, darkened, isActive]) return ( <> @@ -71,27 +182,44 @@ const BaseRouteTile = (route) => { ref={position === 'start' ? setMarkerRef : undefined} opacity={hover || clicked ? 1 : MARKER_OPACITY} zIndexOffset={hover === position ? 2000 : hover || clicked ? 1000 : 0} - position={[route[`${position}_lat`], route[`${position}_lon`]]} - icon={routeMarker(position)} + position={[ + displayRoute[`${position}_lat`], + displayRoute[`${position}_lon`], + ]} + icon={ + displayRoute.reversible + ? routeMarker('start') + : routeMarker(position) + } eventHandlers={{ popupopen: () => setClicked(true), popupclose: () => setClicked(false), mouseover: () => { if (lineRef.current) { - lineRef.current.setStyle({ color: darkened, opacity: 1 }) + lineRef.current.setStyle({ + color: darkened, + opacity: 1, + }) } + applyArrowheadStyle(darkened, 1) setHover(position) }, mouseout: () => { if (lineRef.current && !clicked) { - lineRef.current.setStyle({ color, opacity: MARKER_OPACITY }) + lineRef.current.setStyle({ + color, + opacity: LINE_OPACITY, + }) + } + if (!clicked) { + applyArrowheadStyle(color, LINE_OPACITY) } setHover('') }, }} > @@ -100,37 +228,68 @@ const BaseRouteTile = (route) => { { + click: ({ originalEvent, latlng }) => { originalEvent.preventDefault() - setClicked((prev) => !prev) + setClicked(true) + if (lineRef.current) { + lineRef.current.setStyle({ + color: darkened, + opacity: 1, + }) + } + applyArrowheadStyle(darkened, 1) + if (latlng) { + setLinePopup([latlng.lat, latlng.lng]) + } }, mouseover: ({ target }) => { if (target && !clicked) { - target.setStyle({ color: darkened, opacity: 1 }) + target.setStyle({ + color: darkened, + opacity: 1, + }) + } + if (!clicked) { + applyArrowheadStyle(darkened, 1) } }, mouseout: ({ target }) => { if (target && !clicked) { - target.setStyle({ color, opacity: LINE_OPACITY }) + target.setStyle({ + color, + opacity: LINE_OPACITY, + }) + } + if (!clicked) { + applyArrowheadStyle(color, LINE_OPACITY) } }, }} - dashArray={route.reversible ? undefined : '5, 5'} - positions={waypoints.map((waypoint) => [ - waypoint.lat_degrees, - waypoint.lng_degrees, - ])} + positions={polylinePositions} pathOptions={{ - color: clicked || hover ? darkened : color, - opacity: clicked || hover ? 1 : LINE_OPACITY, + color: isActive ? darkened : color, + opacity: displayRoute.reversible && isActive ? 1 : LINE_OPACITY, weight: 4, }} /> + {linePopup && ( + setLinePopup(null), + close: () => setLinePopup(null), + }} + > + + + )} ) } export const RouteTile = React.memo( BaseRouteTile, - (prev, next) => prev.updated === next.updated, + (prev, next) => + prev.route.updated === next.route.updated && + prev.orientation === next.orientation, ) diff --git a/src/features/route/index.js b/src/features/route/index.js index 9357e658e..13724a8e4 100644 --- a/src/features/route/index.js +++ b/src/features/route/index.js @@ -3,3 +3,5 @@ export * from './routeMarker' export * from './RoutePopup' export * from './RouteTile' +export * from './RouteLayer' +export * from './useRouteStore' diff --git a/src/features/route/useRouteStore.js b/src/features/route/useRouteStore.js new file mode 100644 index 000000000..5d400053e --- /dev/null +++ b/src/features/route/useRouteStore.js @@ -0,0 +1,205 @@ +// @ts-check + +import { create } from 'zustand' + +const PRECISION = 6 + +export const ROUTE_COORD_EPSILON = 1 / 10 ** PRECISION + +/** + * @typedef {{ + * routeId: string, + * orientation: 'forward' | 'reverse', + * }} RouteSelection + */ + +/** + * @typedef {{ + * key: string, + * poiId: string, + * lat: number, + * lon: number, + * isFort: boolean, + * routes: RouteSelection[], + * }} RoutePoiIndex + */ + +/** + * @param {number} lat + * @param {number} lon + * @param {'start' | 'end'} prefix + */ +const formatCoordKey = (lat, lon) => + `${lat.toFixed(PRECISION)}:${lon.toFixed(PRECISION)}` + +const fallbackKey = (lat, lon, prefix) => + `${prefix}:${formatCoordKey(lat, lon)}` + +export const getRouteCoordKey = formatCoordKey + +export const getRoutePoiKey = (route, position) => { + const lat = position === 'start' ? route.start_lat : route.end_lat + const lon = position === 'start' ? route.start_lon : route.end_lon + const fortId = position === 'start' ? route.start_fort_id : route.end_fort_id + return fortId || fallbackKey(lat, lon, position) +} + +/** + * @param {Record} poiIndex + * @param {RoutePoiIndex | null} entry + * @param {Record} routeCache + * @returns {RouteSelection[]} + */ +const collectNearbyRoutes = (poiIndex, entry, routeCache) => { + if (!entry) return [] + const seen = new Set() + /** @type {RouteSelection[]} */ + const combined = [] + Object.values(poiIndex).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) => { + const id = `${ref.routeId}-${ref.orientation}` + if (!seen.has(id) && routeCache[ref.routeId]) { + seen.add(id) + combined.push(ref) + } + }) + } + }) + return combined +} + +/** + * @param {Record} poiIndex + * @param {import('@rm/types').Route} route + * @param {'forward' | 'reverse'} orientation + */ +const addPoiEntry = (poiIndex, route, orientation) => { + const isForward = orientation === 'forward' + const poiId = isForward ? route.start_fort_id : route.end_fort_id + const lat = isForward ? route.start_lat : route.end_lat + const lon = isForward ? route.start_lon : route.end_lon + const key = poiId || fallbackKey(lat, lon, isForward ? 'start' : 'end') + + const existing = poiIndex[key] || { + key, + poiId: poiId || key, + lat, + lon, + isFort: !!poiId, + routes: [], + } + if ( + !existing.routes.some( + (ref) => ref.routeId === route.id && ref.orientation === orientation, + ) + ) { + existing.routes = [...existing.routes, { routeId: route.id, orientation }] + } + poiIndex[key] = existing +} + +/** + * @typedef {{ + * routeCache: Record, + * poiIndex: Record, + * activePoiId: string, + * activeRoutes: RouteSelection[], + * syncRoutes: (routes: import('@rm/types').Route[]) => void, + * selectPoi: (poiId: string) => void, + * clearSelection: () => void, + * }} RouteStore + */ +export const useRouteStore = create( + /** @returns {RouteStore} */ + (set) => ({ + routeCache: {}, + poiIndex: {}, + activePoiId: '', + activeRoutes: [], + syncRoutes: (routes) => { + set((state) => { + const poiIndex = {} + const incomingIds = new Set() + const nextRouteCache = { ...state.routeCache } + + routes.forEach((route) => { + if (!route?.id) return + incomingIds.add(route.id) + nextRouteCache[route.id] = route + addPoiEntry(poiIndex, route, 'forward') + if (route.reversible) { + addPoiEntry(poiIndex, route, 'reverse') + } + }) + + const activeRouteIds = new Set( + state.activeRoutes.map((ref) => ref.routeId), + ) + Object.keys(nextRouteCache).forEach((routeId) => { + if (!incomingIds.has(routeId) && !activeRouteIds.has(routeId)) { + delete nextRouteCache[routeId] + } + }) + + const { activePoiId } = state + const activeEntry = activePoiId ? poiIndex[activePoiId] : null + const nearbyActiveRoutes = collectNearbyRoutes( + poiIndex, + activeEntry, + nextRouteCache, + ) + const nextActiveRoutes = nearbyActiveRoutes.length + ? nearbyActiveRoutes + : state.activeRoutes.filter((ref) => nextRouteCache[ref.routeId]) + + return { + poiIndex, + routeCache: nextRouteCache, + activeRoutes: nextActiveRoutes, + } + }) + }, + selectPoi: (poiId) => { + set((state) => { + if (!poiId) { + return state + } + if (state.activePoiId === poiId) { + return { + ...state, + activePoiId: '', + activeRoutes: [], + } + } + const entry = state.poiIndex[poiId] + if (!entry) { + return state + } + const routes = collectNearbyRoutes( + state.poiIndex, + entry, + state.routeCache, + ) + return { + ...state, + activePoiId: poiId, + activeRoutes: routes, + } + }) + }, + clearSelection: () => + set((state) => + state.activePoiId + ? { + ...state, + activePoiId: '', + activeRoutes: [], + } + : state, + ), + }), +) diff --git a/src/pages/map/components/QueryData.jsx b/src/pages/map/components/QueryData.jsx index 3ecf01d17..81f8a1817 100644 --- a/src/pages/map/components/QueryData.jsx +++ b/src/pages/map/components/QueryData.jsx @@ -11,6 +11,7 @@ import { RobustTimeout } from '@services/apollo/RobustTimeout' import { FILTER_SKIP_LIST } from '@assets/constants' import { Notification } from '@components/Notification' import { GenerateCells } from '@features/s2cell' +import { RouteLayer } from '@features/route' import { useAnalytics } from '@hooks/useAnalytics' import { useProcessError } from '@hooks/useProcessError' @@ -185,14 +186,17 @@ function QueryData({ category, timeout }) { ) : null } + const filteredData = returnData.filter((each) => !hideList.has(each.id)) + + if (category === 'routes') { + return + } + return ( - {returnData.map((each) => { - if (!hideList.has(each.id)) { - return - } - return null - })} + {filteredData.map((each) => ( + + ))} ) } diff --git a/src/services/queries/route.js b/src/services/queries/route.js index a5c48fd5f..c3dee2894 100644 --- a/src/services/queries/route.js +++ b/src/services/queries/route.js @@ -5,8 +5,10 @@ import { gql } from '@apollo/client' const core = gql` fragment CoreRoute on Route { id + start_fort_id start_lat start_lon + end_fort_id end_lat end_lon } diff --git a/yarn.lock b/yarn.lock index e3655a6ea..ca946632e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6313,12 +6313,27 @@ latest-version@^2.0.0: dependencies: package-json "^2.0.0" +leaflet-arrowheads@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/leaflet-arrowheads/-/leaflet-arrowheads-1.4.0.tgz#2a92711c4ca89e6a63014f552cc0d6c67653a773" + integrity sha512-aIjsmoWe1VJXaGOpKpS6E8EzN2vpx3GGCNP/FxQteLVzAg5xMID7elf9hj/1CWLJo8FuGRjSvKkUQDj7mocrYA== + dependencies: + leaflet "^1.7.1" + leaflet-geometryutil "^0.10.0" + +leaflet-geometryutil@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/leaflet-geometryutil/-/leaflet-geometryutil-0.10.3.tgz#dda78546abc58723a46b47fd6ffb081f5b63cec4" + integrity sha512-Qeas+KsnenE0Km/ydt8km3AqFe7kJhVwuLdbCYM2xe2epsxv5UFEaVJiagvP9fnxS8QvBNbm7DJlDA0tkKo9VA== + dependencies: + leaflet "^1.6.0" + leaflet.locatecontrol@0.81.0: version "0.81.0" resolved "https://registry.yarnpkg.com/leaflet.locatecontrol/-/leaflet.locatecontrol-0.81.0.tgz#75e92d07c19edade910a2b5a177ac24cef7d10e7" integrity sha512-5Dqj6VXVFl1vPquYZW95hQYegvzqSI4eLIpZrBMuHuyoAo5i9y6js3z02TF//XXZByIyTI/XBtlxlZLUM08Pcg== -leaflet@1.9.4: +leaflet@1.9.4, leaflet@^1.6.0, leaflet@^1.7.1: version "1.9.4" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d" integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==