diff --git a/package-lock.json b/package-lock.json index babefabc7..83857e795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "copy-to-clipboard": "^3.3.1", "date-fns": "^2.23.0", "firebase": "^9.0.1", + "google-polyline": "^1.0.3", "graphql": "^15.6.0", "lodash.debounce": "^4.0.8", "lz-string": "^1.4.4", @@ -67,6 +68,7 @@ "@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/preset-env": "^7.15.0", "@babel/preset-react": "^7.14.5", + "@types/google-polyline": "^1.0.0", "@types/lodash.debounce": "^4.0.6", "@types/lz-string": "^1.3.34", "@types/node": "^16.7.13", @@ -4379,6 +4381,12 @@ "@types/node": "*" } }, + "node_modules/@types/google-polyline": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/google-polyline/-/google-polyline-1.0.0.tgz", + "integrity": "sha512-XE9nhb1Su6PbirnBy8o/tZcDE/b53fGeUG4pC/Z4/ku54nfm49PXM5IXcCTuL+Df6rXkY6T5yNiYgX90egq+aw==", + "dev": true + }, "node_modules/@types/history": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", @@ -11542,6 +11550,11 @@ "node": ">=10" } }, + "node_modules/google-polyline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/google-polyline/-/google-polyline-1.0.3.tgz", + "integrity": "sha512-36BnqVxmVcR8lTvzO6aeXdICNChAwQLlfMkR1P9IBpTWE8hA6bAbQHCVArwMHB5WIMq9owywtAkjioe3crNq4Q==" + }, "node_modules/got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -27768,6 +27781,12 @@ "@types/node": "*" } }, + "@types/google-polyline": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/google-polyline/-/google-polyline-1.0.0.tgz", + "integrity": "sha512-XE9nhb1Su6PbirnBy8o/tZcDE/b53fGeUG4pC/Z4/ku54nfm49PXM5IXcCTuL+Df6rXkY6T5yNiYgX90egq+aw==", + "dev": true + }, "@types/history": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", @@ -33433,6 +33452,11 @@ "node-forge": "^0.10.0" } }, + "google-polyline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/google-polyline/-/google-polyline-1.0.3.tgz", + "integrity": "sha512-36BnqVxmVcR8lTvzO6aeXdICNChAwQLlfMkR1P9IBpTWE8hA6bAbQHCVArwMHB5WIMq9owywtAkjioe3crNq4Q==" + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", diff --git a/package.json b/package.json index d309f309e..01732feca 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "copy-to-clipboard": "^3.3.1", "date-fns": "^2.23.0", "firebase": "^9.0.1", + "google-polyline": "^1.0.3", "graphql": "^15.6.0", "lodash.debounce": "^4.0.8", "lz-string": "^1.4.4", @@ -85,6 +86,7 @@ "@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/preset-env": "^7.15.0", "@babel/preset-react": "^7.14.5", + "@types/google-polyline": "^1.0.0", "@types/lodash.debounce": "^4.0.6", "@types/lz-string": "^1.3.34", "@types/node": "^16.7.13", diff --git a/src/components/Map/RealtimeVehicleTag/LineOverlay/index.tsx b/src/components/Map/RealtimeVehicleTag/LineOverlay/index.tsx new file mode 100644 index 000000000..457e4ef6a --- /dev/null +++ b/src/components/Map/RealtimeVehicleTag/LineOverlay/index.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { CanvasOverlay } from 'react-map-gl' + +interface Props { + points: Array<[number, number]> + color: string +} + +interface RedrawArgs { + width: number + height: number + ctx: CanvasRenderingContext2D + project: (point: [number, number]) => [number, number] +} + +const LineOverlay = ({ points, color }: Props): JSX.Element | null => { + const redraw = ({ width, height, ctx, project }: RedrawArgs): void => { + ctx.clearRect(0, 0, width, height) + ctx.lineWidth = 4 + ctx.strokeStyle = color + ctx.globalAlpha = 0.4 + ctx.beginPath() + points.forEach((point) => { + const pixel = project([point[1], point[0]]) + ctx.lineTo(pixel[0], pixel[1]) + }) + ctx.stroke() + } + + return +} + +export default LineOverlay diff --git a/src/components/Map/RealtimeVehicleTag/index.tsx b/src/components/Map/RealtimeVehicleTag/index.tsx index 4415cbae9..0ae88a5b7 100644 --- a/src/components/Map/RealtimeVehicleTag/index.tsx +++ b/src/components/Map/RealtimeVehicleTag/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import { Tooltip } from '@entur/tooltip' import { colors } from '@entur/tokens' @@ -14,54 +14,56 @@ import './styles.scss' interface Props { realtimeVehicle: RealtimeVehicle + setHoveredVehicle: (realtimeVehicle: RealtimeVehicle | null) => void + isHovered: boolean } -const RealtimeVehicleTag = ({ realtimeVehicle }: Props): JSX.Element => { - const [isHovered, setIsHovered] = useState(false) - - return ( - } - className={`map__realtime-vehicle-tag-tooltip ${ - isHovered ? 'visible' : '' - }`} - disableHoverListener={true} - isOpen={true} - showCloseButton={false} +const RealtimeVehicleTag = ({ + realtimeVehicle, + setHoveredVehicle, + isHovered, +}: Props): JSX.Element => ( + } + className={`map__realtime-vehicle-tag-tooltip ${ + isHovered ? 'visible' : '' + }`} + disableHoverListener={true} + isOpen={true} + showCloseButton={false} + > +
setHoveredVehicle(realtimeVehicle)} + onMouseLeave={() => setHoveredVehicle(null)} + style={ + realtimeVehicle.active + ? { backgroundColor: 'white' } + : { backgroundColor: colors.greys.grey30 } + } >
setIsHovered(true)} - onMouseOut={() => setIsHovered(false)} + className="map__realtime-vehicle-tag-circle-inner" style={ realtimeVehicle.active - ? { backgroundColor: 'white' } + ? { + backgroundColor: getIconColor( + realtimeVehicle.mode.toLowerCase() as + | TransportMode + | LegMode + | 'ferry', + IconColorType.DEFAULT, + undefined, + ), + } : { backgroundColor: colors.greys.grey30 } } > -
- {realtimeVehicle.line.publicCode} -
+ {realtimeVehicle.line.publicCode}
- - ) -} +
+
+) export default RealtimeVehicleTag diff --git a/src/components/Map/RealtimeVehicleTag/styles.scss b/src/components/Map/RealtimeVehicleTag/styles.scss index de50c70b2..b4c911168 100644 --- a/src/components/Map/RealtimeVehicleTag/styles.scss +++ b/src/components/Map/RealtimeVehicleTag/styles.scss @@ -26,9 +26,11 @@ } &-tooltip { opacity: 0; + visibility: hidden; } &-tooltip.visible { opacity: 1; + visibility: visible; transition: opacity 0.1s ease-in; margin-bottom: 0.5rem; } diff --git a/src/components/Map/index.tsx b/src/components/Map/index.tsx index f7f3aaaa1..67b43e278 100644 --- a/src/components/Map/index.tsx +++ b/src/components/Map/index.tsx @@ -3,22 +3,26 @@ import React, { useState, memo, useRef, useEffect, useMemo } from 'react' import { InteractiveMap, Marker } from 'react-map-gl' import type { MapRef } from 'react-map-gl' +import type { ClusterProperties } from 'supercluster' import useSupercluster from 'use-supercluster' +import polyline from 'google-polyline' -import type { ClusterProperties } from 'supercluster' +import { TransportMode } from '@entur/sdk' import { Station, Vehicle } from '@entur/sdk/lib/mobility/types' import PositionPin from '../../assets/icons/positionPin' -import { StopPlaceWithDepartures } from '../../types' +import { IconColorType, StopPlaceWithDepartures } from '../../types' import { Filter } from '../../services/realtimeVehicles/types/filter' -import { useDebounce } from '../../utils' +import { getIconColor, useDebounce } from '../../utils' -import useVehicleData from '../../logic/useRealtimeVehicleData' +import useRealtimeVehicleData from '../../logic/useRealtimeVehicleData' +import { RealtimeVehicle } from '../../services/realtimeVehicles/types/realtimeVehicle' +import LineOverlay from './RealtimeVehicleTag/LineOverlay' import BikeRentalStationTag from './BikeRentalStationTag' import StopPlaceTag from './StopPlaceTag' import ScooterMarkerTag from './ScooterMarkerTag' @@ -50,12 +54,31 @@ const Map = ({ const debouncedViewport = useDebounce(viewport, 200) const mapRef = useRef(null) const [filter, setFilter] = useState({}) - const { realtimeVehicles } = useVehicleData(filter) + const { realtimeVehicles } = useRealtimeVehicleData(filter) const [bounds, setBounds] = useState<[number, number, number, number]>( mapRef.current?.getMap()?.getBounds()?.toArray()?.flat() || ([0, 0, 0, 0] as [number, number, number, number]), ) + const [hoveredVehicle, setHoveredVehicle] = + useState(null) + + const displayedLine = useMemo(() => { + if (!hoveredVehicle || !hoveredVehicle.line.pointsOnLink) return null + + const coords = polyline.decode(hoveredVehicle.line.pointsOnLink) + + return ( + + ) + }, [hoveredVehicle]) + useEffect(() => { const newBounds = (mapRef.current ?.getMap() @@ -143,15 +166,24 @@ const Map = ({ key={vehicle.vehicleRef} latitude={vehicle.location.latitude} longitude={vehicle.location.longitude} - className="map__live-vehicle-marker" + className="map__realtime-vehicle-marker" + offsetTop={-25} + offsetLeft={-10} > )) : [], - [realtimeVehicles], + [realtimeVehicles, hoveredVehicle], ) const scooterClusterMarkers = useMemo(() => { @@ -175,6 +207,7 @@ const Map = ({ } latitude={slatitude} longitude={slongitude} + className="map__scooter-marker" > {realtimeVehicles && realtimeVehicleMarkers} + {displayedLine} {scooterClusters && scooterClusterMarkers} {stopPlaces && stopPlaceMarkers} {stationClusters && stationClusterMarkers} diff --git a/src/components/Map/styles.scss b/src/components/Map/styles.scss index 804988d0b..f8b30e7f0 100644 --- a/src/components/Map/styles.scss +++ b/src/components/Map/styles.scss @@ -1,3 +1,12 @@ -.map__live-vehicle-marker:hover { - z-index: 1000; +.map__realtime-vehicle-marker { + z-index: 1; + &:hover { + z-index: 3; + } +} + +.map__bike-rental-station-marker, +.map__scooter-marker, +.map__stop-place-marker { + z-index: 2; } diff --git a/src/containers/Admin/EditTab/index.tsx b/src/containers/Admin/EditTab/index.tsx index 07a7ee532..7177a71d9 100644 --- a/src/containers/Admin/EditTab/index.tsx +++ b/src/containers/Admin/EditTab/index.tsx @@ -39,7 +39,7 @@ import { } from '../../../settings/LocalStorage' import { useStopPlacesWithLines } from '../../../logic/useStopPlacesWithLines' -import useVehicleData from '../../../logic/useRealtimeVehicleData' +import useRealtimeVehicleData from '../../../logic/useRealtimeVehicleData' import StopPlacePanel from './StopPlacePanel' import BikePanelSearch from './BikeSearch' @@ -104,7 +104,7 @@ const EditTab = (): JSX.Element => { settings?.distance || DEFAULT_DISTANCE, ) - const { allLinesWithRealtimeData } = useVehicleData() + const { allLinesWithRealtimeData } = useRealtimeVehicleData() const { uniqueLines } = useStopPlacesWithLines() const realtimeLines = useMemo( diff --git a/src/dashboards/Map/styles.scss b/src/dashboards/Map/styles.scss index b1fe08749..f63075fdb 100644 --- a/src/dashboards/Map/styles.scss +++ b/src/dashboards/Map/styles.scss @@ -16,6 +16,7 @@ align-items: center; justify-content: left; overflow-x: scroll; + z-index: 3; } .content { max-width: 100%; diff --git a/src/logic/useRealtimeVehicleData.ts b/src/logic/useRealtimeVehicleData.ts index 0a27ab6fe..d01cff045 100644 --- a/src/logic/useRealtimeVehicleData.ts +++ b/src/logic/useRealtimeVehicleData.ts @@ -35,7 +35,7 @@ interface QueryData { /** * Hook to query and subscribe to remote vehicle data */ -export default function useVehicleData(filter?: Filter): Return { +export default function useRealtimeVehicleData(filter?: Filter): Return { const client = useApolloClient() const [state, dispatch] = useVehicleReducer() const { uniqueLines } = useStopPlacesWithLines() @@ -133,7 +133,11 @@ export default function useVehicleData(filter?: Filter): Return { const line = uniqueLines?.find((l) => l.id === vehicle.line.lineRef) return { ...vehicle, - line: { ...vehicle.line, publicCode: line?.publicCode }, + line: { + ...vehicle.line, + publicCode: line?.publicCode, + pointsOnLink: line?.pointsOnLink, + }, } }) setRealtimeVehicles(mappedDataFromBothAPIs) diff --git a/src/service.ts b/src/service.ts index 0b50d1fcd..e8ce7b6cd 100644 --- a/src/service.ts +++ b/src/service.ts @@ -51,6 +51,9 @@ interface EstimatedCall { transportSubmode: TransportSubmode publicCode: string } + pointsOnLink: { + points: string + } } } @@ -127,6 +130,9 @@ export async function getStopPlacesWithLines( transportSubmode, publicCode } + pointsOnLink { + points + } } } } @@ -143,6 +149,7 @@ export async function getStopPlacesWithLines( .map(({ destinationDisplay, serviceJourney }) => ({ ...serviceJourney.line, name: `${serviceJourney.line.publicCode} ${destinationDisplay.frontText}`, + pointsOnLink: serviceJourney.pointsOnLink.points, })) const uniqueLines = unique( diff --git a/src/services/realtimeVehicles/types/line.ts b/src/services/realtimeVehicles/types/line.ts index 298ed80fd..f3b1bf61d 100644 --- a/src/services/realtimeVehicles/types/line.ts +++ b/src/services/realtimeVehicles/types/line.ts @@ -2,4 +2,5 @@ export type Line = { lineRef: string lineName: string publicCode?: string + pointsOnLink?: string } diff --git a/src/settings/UrlStorage.ts b/src/settings/UrlStorage.ts index 43369a52c..83621109e 100644 --- a/src/settings/UrlStorage.ts +++ b/src/settings/UrlStorage.ts @@ -22,7 +22,6 @@ export const DEFAULT_SETTINGS: Settings = { logoSize: '32px', description: '', hiddenRealtimeDataLineRefs: [], - hideRealtimeData: false, } const VERSION_PREFIX_REGEX = /^v(\d)+::/ diff --git a/src/settings/index.ts b/src/settings/index.ts index 08497763e..26951b27c 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -58,7 +58,7 @@ export interface Settings { hideSituations?: boolean hideTracks?: boolean hideWalkInfo?: boolean - hideRealtimeData: boolean + hideRealtimeData?: boolean hiddenRealtimeDataLineRefs: string[] } @@ -80,6 +80,7 @@ const DEFAULT_SETTINGS: Partial = { owners: [] as string[], hiddenStopModes: {}, hiddenRealtimeDataLineRefs: [], + hideRealtimeData: true, } export function useSettings(): [Settings | null, Setter] { diff --git a/src/types.ts b/src/types.ts index f0fb9c421..9c876e7ea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,7 @@ export interface Line { transportMode: TransportMode transportSubmode: TransportSubmode publicCode: string + pointsOnLink: string } export type StopPlaceWithDepartures = StopPlace & {