Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
Binary file removed public/marker-dot.png
Binary file not shown.
17 changes: 1 addition & 16 deletions src/pages/components/map-related/MapContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon, IconOptions, Layer } from 'leaflet'
import { Layer } from 'leaflet'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TileLayer, useMap } from 'react-leaflet'
Expand All @@ -7,21 +7,6 @@ import { MapPlannedRouteLayer } from './MapLayers/MapPlannedRouteLayer'
import { MapRouteLayer } from './MapLayers/MapRouteLayer'
import { useRecenterOnDataChange } from './useRecenterOnDataChange'

const getIcon = (path: string, width: number = 10, height: number = 10): Icon<IconOptions> => {
return new Icon<IconOptions>({
iconUrl: path,
iconSize: [width, height],
})
}

export const actualRouteLineColor = 'orange'
export const actualRouteStopMarkerPath = `${import.meta.env.BASE_URL}marker-dot.png`
export const actualRouteStopMarker = getIcon(actualRouteStopMarkerPath, 20, 20)

export const plannedRouteLineColor = 'black'
export const plannedRouteStopMarkerPath = `${import.meta.env.BASE_URL}marker-bus-stop.png`
export const plannedRouteStopMarker = getIcon(plannedRouteStopMarkerPath, 20, 25)

export function MapContent({
positionGroups,
plannedRouteStops,
Expand Down
10 changes: 5 additions & 5 deletions src/pages/components/map-related/MapIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import type { ReactNode } from 'react'

export function MapIndex({
lineColor,
imgSrc,
icon,
title,
subtitle,
}: {
lineColor: string
imgSrc: string
/** The marker as it appears on the map. An element rather than a URL, so a glyph drawn inline
* (the bearing arrow) can be painted by the same stylesheet as its counterpart on the map. */
icon: ReactNode
title: string
subtitle?: ReactNode
}) {
return (
<div className="map-index-item">
<div className="map-index-item-config">
<div className="map-index-item-icon">
<img src={imgSrc} alt="" />
</div>
<div className="map-index-item-icon">{icon}</div>
<div className="map-index-item-line" style={{ backgroundColor: lineColor }} />
</div>
<div className="map-index-item-title">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ const meta = {
title: 'Map/Layers/MapIndexLayer',
component: MapIndexLayer,
parameters: { layout: 'centered' },
// Every legend rule in map.scss is nested under `.map-info` and its `.map-legend` slot, so
// mounted bare the legend gets no grid and glyphs at their intrinsic size. Reproduce MapShell's
// two levels; the wrapper needs a size because `.map-legend` is absolutely positioned in it.
decorators: [
(Story) => (
<div className="map-info" style={{ width: 340, height: 210 }}>
<div className="map-legend">
<Story />
</div>
</div>
),
],
} satisfies Meta<typeof MapIndexLayer>

export default meta
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { render, screen, within } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import type { PositionGroup } from '../map-types'
import { SPEED_BANDS } from '../vehicleBearingGlyph'
import { MapIndexLayer } from './MapIndexLayer'

// MapIndexLayer only needs the path/color constants out of MapContent, so stub
// them instead of pulling the whole leaflet map module into the test.
vi.mock('../MapContent', () => ({
actualRouteStopMarkerPath: 'actual-marker.png',
// MapIndexLayer only needs the path/color constants out of mapMarkers, so stub them instead
// of pulling leaflet into the test.
vi.mock('../mapMarkers', () => ({
plannedRouteStopMarkerPath: 'planned-marker.png',
plannedRouteLineColor: 'black',
}))
Expand Down Expand Up @@ -44,7 +44,9 @@ describe('MapIndexLayer', () => {
})

expect(screen.queryByRole('link')).not.toBeInTheDocument()
expect(screen.getByText('(', { exact: false }).closest('bdi')).toHaveTextContent('(99)')
// scoped to the row: the speed key's own title is parenthesised too
const item = document.querySelector('.map-index-item') as HTMLElement
expect(within(item).getByText('(', { exact: false }).closest('bdi')).toHaveTextContent('(99)')
})

it('renders one actual-route legend row per position group', () => {
Expand Down Expand Up @@ -91,4 +93,19 @@ describe('MapIndexLayer', () => {
// title present, but no parenthesised subtitle span
expect(within(item as HTMLElement).queryByText('(', { exact: false })).not.toBeInTheDocument()
})

it('keys the speed ramp only once a ride is on the map to use it on', () => {
const { container, rerender } = renderLayer({ showPlannedRoute: true, positionGroups: [] })
expect(container.querySelector('.map-speed-index')).not.toBeInTheDocument()

rerender(
<MemoryRouter>
<MapIndexLayer showPlannedRoute positionGroups={[group({ label: '12-345-67' })]} />
</MemoryRouter>,
)
const key = container.querySelector('.map-speed-index')!
expect(key).toBeInTheDocument()
// the standing glyph plus one arrow per speed band
expect(key.querySelectorAll('.map-speed-index-band')).toHaveLength(SPEED_BANDS.length + 1)
})
})
15 changes: 8 additions & 7 deletions src/pages/components/map-related/MapLayers/MapIndexLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router'
import type { PositionGroup } from '../map-types'
import {
actualRouteStopMarkerPath,
plannedRouteLineColor,
plannedRouteStopMarkerPath,
} from '../MapContent'
import { MapIndex } from '../MapIndex'
import { plannedRouteLineColor, plannedRouteStopMarkerPath } from '../mapMarkers'
import { MapSpeedIndex } from '../MapSpeedIndex'
import { SPEED_BANDS, VehicleBearingGlyph } from '../vehicleBearingGlyph'

interface MapIndexLayerProps {
showPlannedRoute?: boolean
Expand Down Expand Up @@ -51,7 +49,7 @@
{showPlannedRoute && (
<MapIndex
lineColor={plannedRouteLineColor}
imgSrc={plannedRouteStopMarkerPath}
icon={<img src={plannedRouteStopMarkerPath} alt="" />}
title={t('plannedRoute')}
/>
)}
Expand All @@ -61,11 +59,14 @@
<MapIndex
key={idx}
lineColor={group.color}
imgSrc={actualRouteStopMarkerPath}
// A mid-ramp arrow stands for the whole family here; MapSpeedIndex below spells the
// bands out.
icon={<VehicleBearingGlyph band={SPEED_BANDS[SPEED_BANDS.length - 2]} />}

Check warning on line 64 in src/pages/components/map-related/MapLayers/MapIndexLayer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.at(…)` over `[….length - index]`.

See more on https://sonarcloud.io/project/issues?id=hasadna_open-bus-map-search&issues=AZ_2jF8fg4WqQ2tBTjDp&open=AZ_2jF8fg4WqQ2tBTjDp&pullRequest=1826
title={t('actualRoute')}
subtitle={vehicleSubtitle(group, t)}
/>
))}
{positionGroups.length > 0 && <MapSpeedIndex />}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Marker, Polyline } from 'react-leaflet'
import type { BusStop } from 'src/model/busStop'
import { plannedRouteLineColor, plannedRouteStopMarker } from '../MapContent'
import { plannedRouteLineColor, plannedRouteStopMarker } from '../mapMarkers'

interface MapPlannedRouteLayerProps {
plannedRouteStops?: BusStop[]
Expand Down
55 changes: 45 additions & 10 deletions src/pages/components/map-related/MapLayers/MapRouteLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
import { Marker, Polyline, Popup } from 'react-leaflet'
import { useAgencyList } from 'src/hooks/useAgencyList'
import { busIcon, busIconPath } from '../../utils/BusIcon'
import type { PositionGroup } from '../map-types'
import { actualRouteStopMarker } from '../MapContent'
import type { Point, PositionGroup } from '../map-types'
import { rideEndMarker, vehicleBearingMarker, vehicleStandingMarker } from '../mapMarkers'
import {
bearingZIndex,
BOOKEND_Z_INDEX,
isStanding,
speedBand,
STANDING_Z_INDEX,
} from '../vehicleBearingGlyph'
import { BusToolTip } from './BusToolTip'
import BusToolTipFooter from './BusToolTipFooter'

Expand All @@ -14,6 +21,26 @@
navigateMarkers: (groupIndex: number, id: number, marker: Layer) => void
}

/**
* A ping the vehicle reported moving becomes an arrow along its bearing, grown and filled in by
* how fast; one it reported standing at (velocity exactly 0) becomes the compass badge, which
* still faces the way the bus did. Both read the same `velocity` the tooltip prints, so the
* shape can never contradict the number behind it — and both carry the stacking order that
* keeps the small glyphs on top of the big ones.
*
* (`Point.color` holds that velocity, not a colour — see `toPoint`.)
*/
function pingMarker({ bearing, color: velocity }: Point) {
// Only the standing badge can say "heading unknown" (it drops its needle); a moving ping has
// to point somewhere, and `toPoint` has already defaulted a missing SIRI bearing to 0 anyway.
return isStanding(velocity)
? { icon: vehicleStandingMarker(bearing), zIndexOffset: STANDING_Z_INDEX }
: {
icon: vehicleBearingMarker(bearing ?? 0, velocity),
zIndexOffset: bearingZIndex(speedBand(velocity)),
}
}

export function MapRouteLayer({
positionGroups,
showNavigationButtons,
Expand Down Expand Up @@ -42,22 +69,30 @@
/>
{group.positions.map((pos, i) => {
const markerKey = `${groupIndex}-${i}`
const icon =
// The ride is bookended: its operator's logo where it started, a chequered disc
// where it was last seen. A one-ping ride keeps the logo — it never got to finish.
const { icon, zIndexOffset } =
i === 0
? busIcon({
// eslint-disable-next-line i18next/no-literal-string -- icon lookup key, not user text
operator_id: pos.operator?.toString() || 'default',
name: agencyList.find((agency) => agency.operatorRef === pos.operator)
?.agencyName,
})
: actualRouteStopMarker
? {
icon: busIcon({
// eslint-disable-next-line i18next/no-literal-string -- icon lookup key, not user text
operator_id: pos.operator?.toString() || 'default',
name: agencyList.find((agency) => agency.operatorRef === pos.operator)
?.agencyName,
}),
zIndexOffset: BOOKEND_Z_INDEX,
}
: i === group.positions.length - 1
? { icon: rideEndMarker, zIndexOffset: BOOKEND_Z_INDEX }
: pingMarker(pos)

Check warning on line 87 in src/pages/components/map-related/MapLayers/MapRouteLayer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=hasadna_open-bus-map-search&issues=AZ_2u1AohSPxMcWXd7xo&open=AZ_2u1AohSPxMcWXd7xo&pullRequest=1826
return (
<Marker
ref={(ref) => {
markerRef.current[markerKey] = ref
}}
position={pos.loc}
icon={icon}
zIndexOffset={zIndexOffset}
key={markerKey}>
<Popup minWidth={300} maxWidth={700}>
<BusToolTip position={pos} icon={busIconPath(pos.operator!)}>
Expand Down
34 changes: 34 additions & 0 deletions src/pages/components/map-related/MapSpeedIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useTranslation } from 'react-i18next'
import {
SPEED_BANDS,
speedBandLabel,
STANDING_LABEL,
VehicleBearingGlyph,
VehicleStandingGlyph,
} from './vehicleBearingGlyph'

/**
* Key to the ping markers' speed ramp: the compass badge for a bus reported standing, then an
* arrow per band, growing as it goes faster and red across the slow half.
*/
export function MapSpeedIndex() {
const { t } = useTranslation()

return (
<div className="map-speed-index">
<div className="map-speed-index-title">{`${t('velocity')} (${t('kmh')})`}</div>
<div className="map-speed-index-bands">
<div className="map-speed-index-band">
<VehicleStandingGlyph />
<bdi>{STANDING_LABEL}</bdi>
</div>
{SPEED_BANDS.map((band) => (
<div className="map-speed-index-band" key={band}>
<VehicleBearingGlyph band={band} />
<bdi>{speedBandLabel(band)}</bdi>
</div>
))}
</div>
</div>
)
}
4 changes: 3 additions & 1 deletion src/pages/components/map-related/map-types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { SiriVehicleLocationWithRelatedPydanticModel } from '@hasadna/open-bus-api-client'
import { BusStop } from 'src/model/busStop'

export const ROUTE_COLORS = ['#f97316', '#3b82f6', '#22c55e', '#a855f7', '#ef4444']
/** One per concurrent ride on the map, cycled. Deliberately no red — that belongs to the ping
* speed ramp (`.ping-arrow--slow`), and red arrows over a red line read as neither. */
export const ROUTE_COLORS = ['#f97316', '#3b82f6', '#22c55e', '#a855f7', '#0891b2']

export interface Point {
loc: [number, number]
Expand Down
Loading
Loading