diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json index c0a3b2a25..f6c481626 100644 --- a/client/scripts/size-baseline.json +++ b/client/scripts/size-baseline.json @@ -5,9 +5,9 @@ "src/components/Admin/AdminPluginsPanel.tsx": 1266, "src/components/Budget/CostsPanel.test.tsx": 2730, "src/components/Budget/CostsPanel.tsx": 1798, - "src/components/Map/MapView.tsx": 1385, + "src/components/Map/MapView.tsx": 1337, "src/components/Map/MapViewGL.test.tsx": 2919, - "src/components/Map/MapViewGL.tsx": 2218, + "src/components/Map/MapViewGL.tsx": 2198, "src/components/Planner/DayDetailPanel.test.tsx": 2333, "src/components/Planner/DayPlanSidebar.test.tsx": 5349, "src/components/Planner/DayPlanSidebar.tsx": 2929, diff --git a/client/src/components/Map/MapView.tsx b/client/src/components/Map/MapView.tsx index b1ef0ea0e..7cee1ab75 100644 --- a/client/src/components/Map/MapView.tsx +++ b/client/src/components/Map/MapView.tsx @@ -3,7 +3,7 @@ import DOM from 'react-dom' import { categoryIconSvg } from './categoryIconSvg' import { MapContainer, TileLayer, Marker, Polyline, CircleMarker, Circle, useMap, Tooltip } from 'react-leaflet' import MarkerClusterGroup from 'react-leaflet-cluster' -import { makeMarkerDraggable, makePoiDraggable, draggedPoiId } from './markerDrag' +import { makeMarkerDraggable, makePoiDraggable } from './markerDrag' import { CLUSTER_OPTIONS, createClusterIcon, revealInCluster, type ClusterGroupLike } from './markerCluster' import RoadtripViaMarkers from './RoadtripViaMarkers' import PlannerWaypointMarker from './PlannerWaypointMarker' @@ -50,7 +50,7 @@ import { useTransportRoutes } from '../../hooks/useTransportRoutes' import { visibleRouteReservations } from '../../utils/reservationRoutes' import { safeHexColor } from '../../utils/safeColor' import { placeMarkerLook } from './markerLook' -import { escapeHtml, wrapLongitude } from '@trek/shared' +import { escapeHtml } from '@trek/shared' import type { Day, Place, Reservation, RouteVia } from '../../types' import type { MapHoverInfo } from './mapHover' import { nightPauseMarker, NIGHT_PAUSE_MIN_ZOOM } from './nightPauseMarker' @@ -299,60 +299,6 @@ function CameraHoverGuard({ movingRef, onMoveStart, onZoom }: { movingRef: { cur return null } -/** - * Takes a corridor hit dropped anywhere on the map and reports where it landed. - * - * The container rather than the drawn route: a polyline is a real element here but not in - * the GL renderers, and the drop coordinate answers "where on the drive" just as well - * once the caller projects it onto the routed geometry — one behaviour for all three. - */ -function PoiDropTarget({ onPoiDropOnRoute }: { onPoiDropOnRoute?: (osmId: string, lat: number, lng: number) => void }) { - const map = useMap() - useEffect(() => { - if (!onPoiDropOnRoute) return - const container = map.getContainer() - const onDragOver = (e: DragEvent) => { - if (!draggedPoiId(e)) return - e.preventDefault() - if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy' - } - const onDrop = (e: DragEvent) => { - const osmId = draggedPoiId(e) - if (!osmId) return - e.preventDefault() - const rect = container.getBoundingClientRect() - const at = onRealWorld(map.containerPointToLatLng([e.clientX - rect.left, e.clientY - rect.top])) - onPoiDropOnRoute(osmId, at.lat, at.lng) - } - container.addEventListener('dragover', onDragOver) - container.addEventListener('drop', onDrop) - return () => { - container.removeEventListener('dragover', onDragOver) - container.removeEventListener('drop', onDrop) - } - }, [map, onPoiDropOnRoute]) - return null -} - -// Emits the current viewport bbox on pan/zoom so the POI-explore pill can fetch -// OSM places for the visible area, with the zoom an overlay's minimum is read against. -function ViewportController({ onViewportChange }: { onViewportChange?: MapViewProps['onViewportChange'] }) { - const map = useMap() - useEffect(() => { - if (!onViewportChange) return - const emit = () => { - const b = map.getBounds() - // A view of a world copy (or of a trip drawn past the antimeridian) asks for the real places there. - onViewportChange(bboxOnRealWorld({ south: b.getSouth(), west: b.getWest(), north: b.getNorth(), east: b.getEast() }), map.getZoom()) - } - map.whenReady(emit) // ensure the first bbox is captured once the map is laid out - map.on('moveend', emit) - map.on('zoomend', emit) - return () => { map.off('moveend', emit); map.off('zoomend', emit) } - }, [map, onViewportChange]) - return null -} - function MapCameraSnapshot({ onChange }: { onChange: (center: [number, number], zoom: number) => void }) { const map = useMap() useEffect(() => { @@ -412,28 +358,6 @@ interface MapControllerProps { zoom: number } -/** - * Moves the view along when the longitude frame changes under it (#2975): the trip - * is then drawn one world further east or west, and a view left where it was would - * look at an empty copy. One jump of a whole world, so nothing on screen changes. - */ -function LngFrameFollower() { - const map = useMap() - const frame = useLngFrame() - const prev = useRef(frame) - useEffect(() => { - const before = prev.current - prev.current = frame - if (before === frame || typeof map.getCenter !== 'function') return - const center = map.getCenter() - // Without a frame the trip is back on the real world, and so is the view. - const target = frame ? frameLng(frame, center.lng) : wrapLongitude(center.lng) - const shift = target - center.lng - if (shift !== 0) map.setView([center.lat, target], map.getZoom(), { animate: false }) - }, [frame, map]) - return null -} - function MapController({ center, zoom }: MapControllerProps) { const map = useMap() const frame = useLngFrame() @@ -563,10 +487,6 @@ function BoundsController({ places, routeCoords, fitKey, paddingOpts, framedOnMo return null } -interface MapClickHandlerProps { - onClick: ((e: L.LeafletMouseEvent) => void) | null -} - const TRACK_CASING_PANE = 'trek-track-casing' /** @@ -589,38 +509,6 @@ function TrackCasingPane({ onReady }: { onReady: (ready: boolean) => void }) { return null } -/** - * The event as the map reports it, with its point moved from a repeated world copy - * back onto the real world: a right-click on the copy east of it said 274.9 for - * -85.1, and the place was saved there (#2976). - */ -function onRealWorldEvent(e: L.LeafletMouseEvent): L.LeafletMouseEvent { - const lng = wrapLongitude(e.latlng.lng) - return lng === e.latlng.lng ? e : { ...e, latlng: L.latLng(e.latlng.lat, lng) } -} - -function MapClickHandler({ onClick }: MapClickHandlerProps) { - const map = useMap() - useEffect(() => { - if (!onClick) return - const handle = (e: L.LeafletMouseEvent) => onClick(onRealWorldEvent(e)) - map.on('click', handle) - return () => { map.off('click', handle) } - }, [map, onClick]) - return null -} - -function MapContextMenuHandler({ onContextMenu }: { onContextMenu: ((e: L.LeafletMouseEvent) => void) | null }) { - const map = useMap() - useEffect(() => { - if (!onContextMenu) return - const handle = (e: L.LeafletMouseEvent) => onContextMenu(onRealWorldEvent(e)) - map.on('contextmenu', handle) - return () => { map.off('contextmenu', handle) } - }, [map, onContextMenu]) - return null -} - // Travel times are shown in the day sidebar (per-segment connectors), not on the map. import { useMarkerThumbs } from './useMarkerThumbs' @@ -635,8 +523,8 @@ import { useIsPhone } from '../../mobile/useIsPhone' import type { GeoPosition, TrackingMode } from '../../hooks/useGeolocation' import { selectedPlaceTarget } from './selectedPlaceTarget' import { LngFrameProvider, useLngFrame, useTripLngFrame } from './lngFrameContext' -import { frameLine, frameLng, type LngFrame } from '../../utils/lngFrame' -import { bboxOnRealWorld, onRealWorld } from '../../utils/worldCopy' +import { frameLine, frameLng } from '../../utils/lngFrame' +import { LngFrameFollower, MapClickHandler, MapContextMenuHandler, PoiDropTarget, ViewportController } from './leafletMapBridges' function LeafletLocationLayer({ position: fix, mode }: { position: GeoPosition | null; mode: TrackingMode }) { const map = useMap() diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index 62eef119e..d5464a0dd 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useMemo, useState, useCallback } from 'react' -import { makeMarkerDraggable, makePoiDraggable, draggedPoiId } from './markerDrag' +import { makeMarkerDraggable, makePoiDraggable } from './markerDrag' import { useStableVias } from './viaMarkerState' import { usePlannerWaypointMarkersGL } from './usePlannerWaypointMarkersGL' import { carryAssistantMarks, useAssistantMapGL } from './useAssistantMapGL' @@ -50,6 +50,7 @@ import { pluginsApi, type PluginMapMarker, type PluginMapLayer } from '../../api import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from '../../constants/mapDefaults' import { alongShortestArc, computeMapViewport, TILE_SIZE_GL } from '../../utils/mapViewport' import { onRealWorld } from '../../utils/worldCopy' +import { bindPoiDropTarget } from './poiDropTarget' import { selectedPlaceTarget } from './selectedPlaceTarget' import { PLACE_CLUSTER_CIRCLE_LAYER_ID, PLACE_CLUSTER_COUNT_LAYER_ID, PLACE_CLUSTER_SOURCE_ID, @@ -946,35 +947,12 @@ export function MapViewGL({ } }, [mapReady, onRouteClick]) - /** - * The map takes the drop, not the drawn route: in a GL renderer the line lives in the - * canvas and has no element to aim at. The coordinate under the pointer says just as - * much once it is projected onto the routed geometry, and it works the same in all - * three renderers. - */ + // A POI dropped on the map lands on the drive (see poiDropTarget.ts). useEffect(() => { const map = mapRef.current const container = map?.getContainer?.() if (!container || !mapReady || !onPoiDropOnRoute) return - const onDragOver = (e: DragEvent) => { - if (!draggedPoiId(e)) return - e.preventDefault() - if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy' - } - const onDrop = (e: DragEvent) => { - const osmId = draggedPoiId(e) - if (!osmId) return - e.preventDefault() - const rect = container.getBoundingClientRect() - const at = onRealWorld(map.unproject([e.clientX - rect.left, e.clientY - rect.top])) - onPoiDropOnRoute(osmId, at.lat, at.lng) - } - container.addEventListener('dragover', onDragOver) - container.addEventListener('drop', onDrop) - return () => { - container.removeEventListener('dragover', onDragOver) - container.removeEventListener('drop', onDrop) - } + return bindPoiDropTarget(container, (x, y) => map.unproject([x, y]), onPoiDropOnRoute) }, [mapReady, onPoiDropOnRoute]) const onViewportChangeRef = useRef(onViewportChange) onViewportChangeRef.current = onViewportChange @@ -1378,7 +1356,6 @@ export function MapViewGL({ // timer — dedupe so the form doesn't open twice. if (Date.now() - lastContextFire < 700) return false lastContextFire = Date.now() - // A right-click on a repeated world copy still means the place on the real world (#2976). onClickRefs.current.context?.({ latlng: onRealWorld({ lat: lngLat.lat, lng: lngLat.lng }), originalEvent }) return true } @@ -1985,8 +1962,6 @@ export function MapViewGL({ const fitPoints = routeCoords.length > 0 ? [...routeCoords, ...markerPoints] : markerPoints if (fitPoints.length === 0) return const bounds = new gl.LngLatBounds() - // The short way across the antimeridian, as the focus fit below does: a day - // from Los Angeles to Auckland frames the Pacific, not the rest of the world (#2975). alongShortestArc(fitPoints).forEach(([lat, lng]) => bounds.extend([lng, lat])) let fitted = false const run = () => { diff --git a/client/src/components/Map/leafletMapBridges.tsx b/client/src/components/Map/leafletMapBridges.tsx new file mode 100644 index 000000000..4bd32f4f3 --- /dev/null +++ b/client/src/components/Map/leafletMapBridges.tsx @@ -0,0 +1,120 @@ +// The Leaflet map's bridges to the page: the events it hands out, with their +// points on the real world (#2976), and the view kept on the world copy the +// trip is drawn on (#2975). + +import { wrapLongitude } from '@trek/shared'; +import L from 'leaflet'; +import { useEffect, useRef } from 'react'; +import { useMap } from 'react-leaflet'; +import { frameLng, type LngFrame } from '../../utils/lngFrame'; +import { bboxOnRealWorld } from '../../utils/worldCopy'; +import { useLngFrame } from './lngFrameContext'; +import type { MapViewProps } from './mapViewContract'; +import { bindPoiDropTarget } from './poiDropTarget'; + +/** + * Takes a corridor hit dropped anywhere on the map and reports where it landed. + * + * The container rather than the drawn route: a polyline is a real element here but not in + * the GL renderers, and the drop coordinate answers "where on the drive" just as well + * once the caller projects it onto the routed geometry — one behaviour for all three. + */ +export function PoiDropTarget({ + onPoiDropOnRoute, +}: { + onPoiDropOnRoute?: (osmId: string, lat: number, lng: number) => void; +}) { + const map = useMap(); + useEffect(() => { + if (!onPoiDropOnRoute) return; + return bindPoiDropTarget(map.getContainer(), (x, y) => map.containerPointToLatLng([x, y]), onPoiDropOnRoute); + }, [map, onPoiDropOnRoute]); + return null; +} + +// Emits the current viewport bbox on pan/zoom so the POI-explore pill can fetch +// OSM places for the visible area, with the zoom an overlay's minimum is read against. +export function ViewportController({ onViewportChange }: { onViewportChange?: MapViewProps['onViewportChange'] }) { + const map = useMap(); + useEffect(() => { + if (!onViewportChange) return; + const emit = () => { + const b = map.getBounds(); + // A view of a world copy (or of a trip drawn past the antimeridian) asks for the real places there. + onViewportChange( + bboxOnRealWorld({ south: b.getSouth(), west: b.getWest(), north: b.getNorth(), east: b.getEast() }), + map.getZoom() + ); + }; + map.whenReady(emit); // ensure the first bbox is captured once the map is laid out + map.on('moveend', emit); + map.on('zoomend', emit); + return () => { + map.off('moveend', emit); + map.off('zoomend', emit); + }; + }, [map, onViewportChange]); + return null; +} + +/** + * Moves the view along when the longitude frame changes under it (#2975): the trip + * is then drawn one world further east or west, and a view left where it was would + * look at an empty copy. One jump of a whole world, so nothing on screen changes. + */ +export function LngFrameFollower() { + const map = useMap(); + const frame = useLngFrame(); + const prev = useRef(frame); + useEffect(() => { + const before = prev.current; + prev.current = frame; + if (before === frame || typeof map.getCenter !== 'function') return; + const center = map.getCenter(); + // Without a frame the trip is back on the real world, and so is the view. + const target = frame ? frameLng(frame, center.lng) : wrapLongitude(center.lng); + const shift = target - center.lng; + if (shift !== 0) map.setView([center.lat, target], map.getZoom(), { animate: false }); + }, [frame, map]); + return null; +} + +interface MapClickHandlerProps { + onClick: ((e: L.LeafletMouseEvent) => void) | null; +} + +/** + * The event as the map reports it, with its point moved from a repeated world copy + * back onto the real world: a right-click on the copy east of it said 274.9 for + * -85.1, and the place was saved there (#2976). + */ +function onRealWorldEvent(e: L.LeafletMouseEvent): L.LeafletMouseEvent { + const lng = wrapLongitude(e.latlng.lng); + return lng === e.latlng.lng ? e : { ...e, latlng: L.latLng(e.latlng.lat, lng) }; +} + +export function MapClickHandler({ onClick }: MapClickHandlerProps) { + const map = useMap(); + useEffect(() => { + if (!onClick) return; + const handle = (e: L.LeafletMouseEvent) => onClick(onRealWorldEvent(e)); + map.on('click', handle); + return () => { + map.off('click', handle); + }; + }, [map, onClick]); + return null; +} + +export function MapContextMenuHandler({ onContextMenu }: { onContextMenu: ((e: L.LeafletMouseEvent) => void) | null }) { + const map = useMap(); + useEffect(() => { + if (!onContextMenu) return; + const handle = (e: L.LeafletMouseEvent) => onContextMenu(onRealWorldEvent(e)); + map.on('contextmenu', handle); + return () => { + map.off('contextmenu', handle); + }; + }, [map, onContextMenu]); + return null; +} diff --git a/client/src/components/Map/poiDropTarget.ts b/client/src/components/Map/poiDropTarget.ts new file mode 100644 index 000000000..84e923866 --- /dev/null +++ b/client/src/components/Map/poiDropTarget.ts @@ -0,0 +1,39 @@ +// A corridor hit dropped anywhere on the map, for every renderer. +// +// The map takes the drop, not the drawn route: in a GL renderer the line lives in the +// canvas and has no element to aim at, and the coordinate under the pointer says just +// as much once the caller projects it onto the routed geometry. The point goes out on +// the real world, whichever world copy it was dropped on (#2976). + +import { onRealWorld } from '../../utils/worldCopy'; +import { draggedPoiId } from './markerDrag'; + +/** Where a point of the container lies, in the renderer's own coordinates. */ +type PointAt = (x: number, y: number) => { lat: number; lng: number }; + +/** Listens for POI drops on `container` and reports each one; returns the cleanup. */ +export function bindPoiDropTarget( + container: HTMLElement, + pointAt: PointAt, + onPoiDropOnRoute: (osmId: string, lat: number, lng: number) => void +): () => void { + const onDragOver = (e: DragEvent) => { + if (!draggedPoiId(e)) return; + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + }; + const onDrop = (e: DragEvent) => { + const osmId = draggedPoiId(e); + if (!osmId) return; + e.preventDefault(); + const rect = container.getBoundingClientRect(); + const at = onRealWorld(pointAt(e.clientX - rect.left, e.clientY - rect.top)); + onPoiDropOnRoute(osmId, at.lat, at.lng); + }; + container.addEventListener('dragover', onDragOver); + container.addEventListener('drop', onDrop); + return () => { + container.removeEventListener('dragover', onDragOver); + container.removeEventListener('drop', onDrop); + }; +} diff --git a/client/src/components/Map/transportHops.test.ts b/client/src/components/Map/transportHops.test.ts index f302f818e..1a0dbabf2 100644 --- a/client/src/components/Map/transportHops.test.ts +++ b/client/src/components/Map/transportHops.test.ts @@ -93,7 +93,8 @@ describe('transportHop', () => { endpoint({ role: 'from', lat: 33.94, lng: -118.41 }), endpoint({ role: 'to', sequence: 1, lat: -37.01, lng: 174.79 }), ]); - const hop = transportHop(r, true, { seam: 28 })!; + const hop = transportHop(r, true, { seam: 28 }); + if (!hop) throw new Error('a flight with two located ends draws a hop'); expect(hop.from.lng).toBeCloseTo(241.59, 5); expect(hop.to.lng).toBeCloseTo(174.79, 5); expect(hop.arcs).toHaveLength(1); diff --git a/client/src/utils/lngFrame.test.ts b/client/src/utils/lngFrame.test.ts index 779b23ef2..205a6a5ce 100644 --- a/client/src/utils/lngFrame.test.ts +++ b/client/src/utils/lngFrame.test.ts @@ -15,7 +15,7 @@ describe('tripLngFrame (#2975)', () => { it('seams a trip across the Pacific in the middle of the stretch it leaves empty', () => { const frame = tripLngFrame([LA, -118.3, AUCKLAND, 174.76]); expect(frame).not.toBeNull(); - expect(frame!.seam).toBeCloseTo((-118.3 + 174.76) / 2, 5); + expect(frame?.seam).toBeCloseTo((-118.3 + 174.76) / 2, 5); expect(frameLng(frame, LA)).toBeCloseTo(LA + 360, 9); expect(frameLng(frame, AUCKLAND)).toBeCloseTo(AUCKLAND, 9); }); @@ -36,7 +36,8 @@ describe('tripLngFrame (#2975)', () => { }); it('reads a longitude from a world copy where it really is', () => { - expect(tripLngFrame([LA + 360, AUCKLAND - 720])!.seam).toBeCloseTo(tripLngFrame([LA, AUCKLAND])!.seam, 9); + const fromCopies = tripLngFrame([LA + 360, AUCKLAND - 720]); + expect(fromCopies?.seam).toBeCloseTo(tripLngFrame([LA, AUCKLAND])?.seam ?? Number.NaN, 9); }); }); diff --git a/server/src/db/migrations/Migration20200101046350_a_place_lies_on_the_real_world.ts b/server/src/db/migrations/Migration20200101046350_a_place_lies_on_the_real_world.ts index 7cf77c34d..21bcd95b0 100644 --- a/server/src/db/migrations/Migration20200101046350_a_place_lies_on_the_real_world.ts +++ b/server/src/db/migrations/Migration20200101046350_a_place_lies_on_the_real_world.ts @@ -6,16 +6,12 @@ import { Migration } from '@mikro-orm/migrations'; */ const TABLES = ['places', 'reservation_endpoints', 'collection_places', 'place_contrib_outbox'] as const; -interface LngRow { - id: number; - lng: number; -} - -/** Into [-180, 180], the same arithmetic as wrapLongitude in @trek/shared, kept here so this body never changes. */ -function onTheRealWorld(lng: number): number { - const wrapped = ((((lng + 180) % 360) + 360) % 360) - 180; - return wrapped === 0 ? 0 : wrapped; -} +/** + * How many whole worlds `lng` lies east of the real one: floor((lng + 180) / 360), + * spelled with CAST, which truncates towards zero, because SQLite has no floor() + * without its math extension. + */ +const WORLDS = '(CAST((lng + 180) / 360.0 AS INTEGER) - ((lng + 180) / 360.0 < CAST((lng + 180) / 360.0 AS INTEGER)))'; /** * Brings every stored longitude back onto the real world. @@ -30,14 +26,9 @@ function onTheRealWorld(lng: number): number { export class Migration20200101046350_a_place_lies_on_the_real_world extends Migration { override name = 'Migration20200101046350_a_place_lies_on_the_real_world'; - override async up(): Promise { + override up(): void { for (const table of TABLES) { - const rows = (await this.execute( - `SELECT id, lng FROM ${table} WHERE lng IS NOT NULL AND (lng < -180 OR lng > 180)`, - )) as LngRow[]; - for (const row of rows) { - await this.execute(`UPDATE ${table} SET lng = ? WHERE id = ?`, [onTheRealWorld(row.lng), row.id]); - } + this.addSql(`UPDATE ${table} SET lng = lng - 360 * ${WORLDS} WHERE lng < -180 OR lng > 180`); } } } diff --git a/server/tests/unit/db/place-real-world-migration.test.ts b/server/tests/unit/db/place-real-world-migration.test.ts index 8237986bf..9965f7c16 100644 --- a/server/tests/unit/db/place-real-world-migration.test.ts +++ b/server/tests/unit/db/place-real-world-migration.test.ts @@ -60,7 +60,7 @@ async function seed(orm: MikroORM): Promise { await rawExec( orm, `INSERT INTO place_contrib_outbox (id, contrib_key, field, value, place_id, place_name, lat, lng, cid, eligible_at, next_attempt_at) - VALUES (1, 'k1', 'name', 'x', 1, 'right-hand copy', 10.14, 274.921875, 'c1', '2026-01-01', '2026-01-01')`, + VALUES (1, 'k1', 'website', 'x', 1, 'right-hand copy', 10.14, 274.921875, 'c1', '2026-01-01', '2026-01-01')`, ); } diff --git a/server/tests/unit/db/repositories/longitude-on-write.test.ts b/server/tests/unit/db/repositories/longitude-on-write.test.ts index 52c883d70..9a1ec4416 100644 --- a/server/tests/unit/db/repositories/longitude-on-write.test.ts +++ b/server/tests/unit/db/repositories/longitude-on-write.test.ts @@ -108,16 +108,28 @@ describe('longitude on write', () => { it('LNGWRITE-002: a place on the real world, the edges included, is written as it came', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - for (const lng of [-118.4, 180, -180]) { - const id = await places.insertTourPlace({ - trip_id: trip.id, - name: `At ${lng}`, - lat: 0, - lng, - route_geometry: null, - }); - expect(await storedLng(id)).toBe(lng); - } + const west = await places.insertTourPlace({ + trip_id: trip.id, + name: 'West', + lat: 0, + lng: -118.4, + route_geometry: null, + }); + const edge = await places.insertTourPlace({ + trip_id: trip.id, + name: 'Edge', + lat: 0, + lng: 180, + route_geometry: null, + }); + const otherEdge = await places.insertTourPlace({ + trip_id: trip.id, + name: 'Other edge', + lat: 0, + lng: -180, + route_geometry: null, + }); + expect([await storedLng(west), await storedLng(edge), await storedLng(otherEdge)]).toEqual([-118.4, 180, -180]); }); it('LNGWRITE-003: tour places and their route rewrites wrap too', async () => {