Skip to content

Commit

Permalink
Adds Route Map overlay feature
Browse files Browse the repository at this point in the history
Fixes #24 - Trip Details List and Map Overlay
Fixes #29 - The bus's route should also be projected onto the map
Fixes #30 - Hide unrelated stops on the map while a route line is being displayed

Co-authored-by: tarunsinghofficial <[email protected]>
  • Loading branch information
tarunsinghofficial authored and aaronbrethorst committed Aug 14, 2024
1 parent a51553d commit 6b2161f
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 15 deletions.
55 changes: 45 additions & 10 deletions src/components/map/GoogleMap.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@
PUBLIC_OBA_REGION_CENTER_LAT as initialLat,
PUBLIC_OBA_REGION_CENTER_LNG as initialLng
} from '$env/static/public';
import { pushState } from '$app/navigation';
import { debounce } from '$lib/utils';
import { pushState } from '$app/navigation';
import { createMap, loadGoogleMapsLibrary, nightModeStyles } from '$lib/googleMaps';
import LocationButton from '$lib/LocationButton/LocationButton.svelte';
import StopMarker from './StopMarker.svelte';
import RouteMap from './RouteMap.svelte';
import { debounce } from '$lib/utils';
export let selectedTrip;
export let selectedRoute = null;
export let showRoute = false;
const dispatch = createEventDispatcher();
let map = null;
let markers = [];
let allStops = [];
async function loadStopsForLocation(lat, lng) {
const response = await fetch(`/api/oba/stops-for-location?lat=${lat}&lng=${lng}`);
Expand Down Expand Up @@ -50,19 +55,41 @@
async function loadStopsAndAddMarkers(lat, lng) {
const json = await loadStopsForLocation(lat, lng);
const stops = json.data.list;
const newStops = json.data.list;
for (const s of stops) {
if (markerExists(s)) {
continue;
}
allStops = [...new Map([...allStops, ...newStops].map((stop) => [stop.id, stop])).values()];
clearAllMarkers();
addMarker(s);
if (showRoute && selectedRoute) {
const stopsToShow = allStops.filter((s) => s.routeIds.includes(selectedRoute.id));
stopsToShow.forEach((s) => addMarker(s));
} else {
newStops.forEach((s) => addMarker(s));
}
}
function markerExists(s) {
return markers.some((marker) => marker.s.id === s.id);
function clearAllMarkers() {
markers.forEach(({ marker, overlay, element }) => {
marker?.setMap(null);
if (overlay) {
overlay.setMap(null);
overlay.draw = () => {};
overlay.onRemove?.();
}
element?.parentNode?.removeChild(element);
});
markers = [];
}
$: if (selectedRoute && showRoute) {
clearAllMarkers();
const stopsToShow = allStops.filter((s) => s.routeIds.includes(selectedRoute.id));
stopsToShow.forEach((s) => addMarker(s));
} else if (!showRoute || !selectedRoute) {
clearAllMarkers();
allStops.forEach((s) => addMarker(s));
}
function addMarker(s) {
Expand Down Expand Up @@ -107,6 +134,9 @@
container.style.position = 'absolute';
this.getPanes().overlayMouseTarget.appendChild(container);
};
overlay.onRemove = function () {
container?.parentNode?.removeChild(container);
};
markers.push({ s, marker, overlay, element: container });
}
Expand Down Expand Up @@ -161,6 +191,11 @@
</script>
<div id="map"></div>
{#if selectedTrip}
<RouteMap {map} tripId={selectedTrip.tripId} />
{/if}
<LocationButton on:locationObtained={handleLocationObtained} />
<style>
Expand Down
83 changes: 83 additions & 0 deletions src/components/map/RouteMap.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { drawPolyline, loadGoogleMapsLibrary } from '$lib/googleMaps';
export let map;
export let tripId;
let shapeId = null;
let polyline;
let stopMarkers = [];
let infoWindow;
let tripData = null;
let shapeData = null;
onMount(async () => {
await loadGoogleMapsLibrary();
await loadRouteData();
});
onDestroy(() => {
polyline?.setMap(null);
stopMarkers.forEach((marker) => marker.setMap(null));
infoWindow?.close();
});
async function loadRouteData() {
const tripResponse = await fetch(`/api/oba/trip-details/${tripId}`);
tripData = await tripResponse.json();
const tripReferences = tripData?.data?.references?.trips;
const moreTripData = tripReferences?.find((t) => t.id == tripId);
shapeId = moreTripData?.shapeId;
if (shapeId) {
const shapeResponse = await fetch(`/api/oba/shape/${shapeId}`);
shapeData = await shapeResponse.json();
const points = shapeData?.data?.entry?.points;
if (points) {
drawPolyline(points, map);
}
}
const stopTimes = tripData?.data?.entry?.schedule?.stopTimes ?? [];
const stops = tripData?.data?.references?.stops ?? [];
for (const stopTime of stopTimes) {
const stop = stops.find((s) => s.id === stopTime.stopId);
if (stop) {
addStopMarker(stopTime, stop);
}
}
}
function addStopMarker(stopTime, stop) {
const marker = new google.maps.Marker({

Check failure on line 56 in src/components/map/RouteMap.svelte

View workflow job for this annotation

GitHub Actions / lint

'google' is not defined
position: { lat: stop.lat, lng: stop.lon },
map: map,
icon: {
path: google.maps.SymbolPath.CIRCLE,

Check failure on line 60 in src/components/map/RouteMap.svelte

View workflow job for this annotation

GitHub Actions / lint

'google' is not defined
scale: 5,
fillColor: '#FFFFFF',
fillOpacity: 1,
strokeWeight: 1,
strokeColor: '#000000'
}
});
marker.addListener('click', () => {
infoWindow?.close();
infoWindow = new google.maps.InfoWindow({

Check failure on line 72 in src/components/map/RouteMap.svelte

View workflow job for this annotation

GitHub Actions / lint

'google' is not defined
content: `<div>
<h3>${stop.name}</h3>
<p>Arrival time: ${new Date(stopTime.arrivalTime * 1000).toLocaleTimeString()}</p>
</div>`
});
infoWindow.open(map, marker);
});
stopMarkers.push(marker);
}
</script>
4 changes: 4 additions & 0 deletions src/components/oba/StopPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import ArrivalDeparture from '../ArrivalDeparture.svelte';
import { onMount } from 'svelte';
import TripDetailsModal from '../navigation/TripDetailsModal.svelte';
import { createEventDispatcher } from 'svelte';
export let stop;
export let arrivalsAndDeparturesResponse = null;
Expand All @@ -13,6 +14,8 @@
let selectedTripDetails = null;
let interval;
const dispatch = createEventDispatcher();
async function loadData(stopID) {
loading = true;
const response = await fetch(`/api/oba/arrivals-and-departures-for-stop/${stopID}`);
Expand Down Expand Up @@ -62,6 +65,7 @@
scheduledArrivalTime: event.detail.scheduledArrivalTime
};
showTripDetails = true;
dispatch('tripSelected', selectedTripDetails);
}
function handleCloseTripDetailModal() {
showTripDetails = false;
Expand Down
3 changes: 0 additions & 3 deletions src/components/oba/TripDetailsPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@
currentStopIndex = -1;
}
console.log('Current Stop Index:', currentStopIndex);
console.log('Closest Stop:', tripDetails.status?.closestStop);
calculateBusPosition();
} catch (err) {
console.error('Error fetching trip details:', err);
Expand Down
16 changes: 16 additions & 0 deletions src/lib/googleMaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,19 @@ export function nightModeStyles() {
}
];
}

export async function drawPolyline(shape, googleMap) {
await window.google.maps.importLibrary('geometry');
const decodedPath = google.maps.geometry.encoding.decodePath(shape);
const path = decodedPath.map((point) => ({ lat: point.lat(), lng: point.lng() }));

const polyline = new window.google.maps.Polyline({
path,
geodesic: true,
strokeColor: '#00FF00',
strokeOpacity: 1.0,
strokeWeight: 4
});

polyline.setMap(googleMap);
}
33 changes: 31 additions & 2 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,49 @@
import StopPane from '../components/oba/StopPane.svelte';
let stop;
let selectedTrip = null;
let showRoute = false;
let selectedRoute = null;
function stopSelected(event) {
stop = event.detail.stop;
}
function closePane() {
stop = null;
selectedTrip = null;
selectedRoute = null;
showRoute = false;
clearAllMarkers();

Check failure on line 20 in src/routes/+page.svelte

View workflow job for this annotation

GitHub Actions / lint

'clearAllMarkers' is not defined
}
function tripSelected(event) {
selectedTrip = event.detail;
showRoute = true;
selectedRoute = {
id: event.detail.routeId,
shortName: event.detail.routeShortName
};
}
function toggleRoute() {
showRoute = !showRoute;
if (!showRoute) {
selectedRoute = null;
}
}
</script>

{#if stop}
<ModalPane on:close={closePane}>
<StopPane {stop} />
<StopPane
{stop}
on:tripSelected={tripSelected}
{showRoute}
{toggleRoute}
selectedRouteId={selectedRoute?.id}
/>
</ModalPane>
{/if}

<GoogleMap on:stopSelected={stopSelected} />
<GoogleMap {selectedTrip} {selectedRoute} on:stopSelected={stopSelected} {showRoute} />
24 changes: 24 additions & 0 deletions src/routes/api/oba/shape/[shapeId]/+server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { error, json } from '@sveltejs/kit';
import { PUBLIC_OBA_SERVER_URL as baseURL } from '$env/static/public';
import { PRIVATE_OBA_API_KEY as apiKey } from '$env/static/private';

export async function GET({ params }) {
const { shapeId } = params;

let apiURL = `${baseURL}/api/where/shape/${shapeId}.json?key=${apiKey}`;

try {
const response = await fetch(apiURL);
console.log('response:', response);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
return json(data);
} catch (err) {
console.error('Error fetching shape data:', err);
throw error(500, 'Unable to fetch shape data.');
}
}

0 comments on commit 6b2161f

Please sign in to comment.