From 2dfc84f294418da94658ffbda245091963058155 Mon Sep 17 00:00:00 2001 From: Aaron Brethorst Date: Wed, 13 Nov 2024 08:26:20 -0800 Subject: [PATCH 01/47] Foundational work for trip planner integration --- .env.example | 7 ++ README.md | 5 + src/components/search/SearchPane.svelte | 116 +++++++++++++----------- src/routes/api/otp/plan/+server.js | 31 +++++++ 4 files changed, 104 insertions(+), 55 deletions(-) create mode 100644 src/routes/api/otp/plan/+server.js diff --git a/.env.example b/.env.example index b6e2a1a..f2bb6c1 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,20 @@ PRIVATE_OBA_API_KEY="test" + PRIVATE_OBA_GEOCODER_API_KEY="" PRIVATE_OBA_GEOCODER_PROVIDER="google" + PRIVATE_OBACO_API_BASE_URL=https://onebusaway.co/api/v1/regions/:REGION_ID PRIVATE_OBACO_SHOW_TEST_ALERTS=false + PUBLIC_NAV_BAR_LINKS={"Home": "/","About": "/about","Contact": "/contact","Fares & Tolls": "/fares-and-tolls"} PUBLIC_OBA_GOOGLE_MAPS_API_KEY="" PUBLIC_OBA_LOGO_URL="https://onebusaway.org/wp-content/uploads/oba_logo-1.png" PUBLIC_OBA_MAP_PROVIDER="osm" + PUBLIC_OBA_REGION_CENTER_LAT=47.60728155903877 PUBLIC_OBA_REGION_CENTER_LNG=-122.3339240843084 PUBLIC_OBA_REGION_NAME="Puget Sound" + PUBLIC_OBA_SERVER_URL="https://api.pugetsound.onebusaway.org/" + +PUBLIC_OTP_SERVER_URL="" diff --git a/README.md b/README.md index d4f8635..e36ce80 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ See `.env.example` for an example of the required keys and values. - `PRIVATE_OBA_GEOCODER_API_KEY` - string: Your Geocoder service's API key. Leave this blank if you don't have one. - `PRIVATE_OBA_GEOCODER_PROVIDER` - string: Your Geocoder service. We currently only support the Google Places SDK (value: "google"). + +### Trip Planner + +- `PUBLIC_OTP_SERVER_URL` - string: Your OpenTripPlanner 1.x-compatible trip planner server URL. Leave this blank if you don't have one. + ## Building To create a production version of your app: diff --git a/src/components/search/SearchPane.svelte b/src/components/search/SearchPane.svelte index a37d00d..963f26c 100644 --- a/src/components/search/SearchPane.svelte +++ b/src/components/search/SearchPane.svelte @@ -8,6 +8,7 @@ import { t } from 'svelte-i18n'; import { clearVehicleMarkersMap, fetchAndUpdateVehicles } from '$lib/vehicleUtils'; import { calculateMidpoint } from '$lib/mathUtils'; + import { Tabs, TabItem } from 'flowbite-svelte'; const dispatch = createEventDispatcher(); @@ -107,63 +108,68 @@ }); -
-
- - - {#if query} -

- {$t('search.results_for')} "{query}". - -

- {/if} - -
- {#if location} - handleLocationClick(location)} - title={location.formatted_address} - icon={faMapPin} - subtitle={location.types.join(', ')} - /> - {/if} - - {#if routes?.length > 0} - {#each routes as route} - handleRouteClick(route)} - icon={prioritizedRouteTypeForDisplay(route.type)} - title={`${$t('route')} ${route.nullSafeShortName || route.id}`} - subtitle={route.description} - /> - {/each} +
+ + + + + {#if query} +

+ {$t('search.results_for')} "{query}". + +

{/if} - {#if stops?.length > 0} - {#each stops as stop} +
+ {#if location} handleStopClick(stop)} - icon={faSignsPost} - title={stop.name} - subtitle={`${compassDirection(stop.direction)}; Code: ${stop.code}`} + on:click={() => handleLocationClick(location)} + title={location.formatted_address} + icon={faMapPin} + subtitle={location.types.join(', ')} /> - {/each} - {/if} -
- -
- - - {$t('search.for_a_list_of_available_routes')} -
-
+ {/if} + + {#if routes?.length > 0} + {#each routes as route} + handleRouteClick(route)} + icon={prioritizedRouteTypeForDisplay(route.type)} + title={`${$t('route')} ${route.nullSafeShortName || route.id}`} + subtitle={route.description} + /> + {/each} + {/if} + + {#if stops?.length > 0} + {#each stops as stop} + handleStopClick(stop)} + icon={faSignsPost} + title={stop.name} + subtitle={`${compassDirection(stop.direction)}; Code: ${stop.code}`} + /> + {/each} + {/if} +
+ +
+ + + {$t('search.for_a_list_of_available_routes')} +
+ + + plan a trip UI goes here! + +
diff --git a/src/routes/api/otp/plan/+server.js b/src/routes/api/otp/plan/+server.js new file mode 100644 index 0000000..1f76039 --- /dev/null +++ b/src/routes/api/otp/plan/+server.js @@ -0,0 +1,31 @@ +import { error, json } from '@sveltejs/kit'; + +export async function GET() { + try { + const response = await fetch( + 'https://otp.prod.sound.obaweb.org/otp/routers/default/plan?fromPlace=47.5423055%2C-122.38677&toPlace=47.639376%2C-122.128238', + { + headers: { + 'Accept': 'application/json' + } + } + ); + + if (!response.ok) { + throw error(response.status, `OpenTripPlanner API returned status ${response.status}`); + } + + const data = await response.json(); + return json(data); + + } catch (err) { + // If it's already a SvelteKit error, rethrow it + if (err.status) throw err; + + // Otherwise wrap it in a 500 error + throw error(500, { + message: 'Failed to fetch trip planning data', + error: err.message + }); + } +} \ No newline at end of file From e203f048f20b0d4eb772cbb717adb3aedf83b293 Mon Sep 17 00:00:00 2001 From: Aaron Brethorst Date: Tue, 5 Nov 2024 21:27:34 -0800 Subject: [PATCH 02/47] Flowbite Tab Item fit and finish --- src/components/search/SearchPane.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/search/SearchPane.svelte b/src/components/search/SearchPane.svelte index 963f26c..7371314 100644 --- a/src/components/search/SearchPane.svelte +++ b/src/components/search/SearchPane.svelte @@ -109,7 +109,7 @@
- + From af794d463ed3ec70536ea45b7432c087aacbdacf Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Sat, 9 Nov 2024 21:59:09 -0800 Subject: [PATCH 03/47] refactor: trip planning API to accept dynamic parameters for fromPlace and toPlace --- src/routes/api/otp/plan/+server.js | 54 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/routes/api/otp/plan/+server.js b/src/routes/api/otp/plan/+server.js index 1f76039..33386fe 100644 --- a/src/routes/api/otp/plan/+server.js +++ b/src/routes/api/otp/plan/+server.js @@ -1,31 +1,35 @@ import { error, json } from '@sveltejs/kit'; -export async function GET() { - try { - const response = await fetch( - 'https://otp.prod.sound.obaweb.org/otp/routers/default/plan?fromPlace=47.5423055%2C-122.38677&toPlace=47.639376%2C-122.128238', - { - headers: { - 'Accept': 'application/json' - } - } - ); +export async function GET({ url }) { + const fromPlace = url.searchParams.get('fromPlace'); + const toPlace = url.searchParams.get('toPlace'); - if (!response.ok) { - throw error(response.status, `OpenTripPlanner API returned status ${response.status}`); - } + if (!fromPlace || !toPlace) { + throw error(400, 'Missing required parameters: fromPlace and toPlace'); + } - const data = await response.json(); - return json(data); + try { + const response = await fetch( + `https://otp.prod.sound.obaweb.org/otp/routers/default/plan?fromPlace=${encodeURIComponent(fromPlace)}&toPlace=${encodeURIComponent(toPlace)}`, + { + headers: { + Accept: 'application/json' + } + } + ); - } catch (err) { - // If it's already a SvelteKit error, rethrow it - if (err.status) throw err; + if (!response.ok) { + throw error(response.status, `OpenTripPlanner API returned status ${response.status}`); + } - // Otherwise wrap it in a 500 error - throw error(500, { - message: 'Failed to fetch trip planning data', - error: err.message - }); - } -} \ No newline at end of file + const data = await response.json(); + return json(data); + } catch (err) { + if (err.status) throw err; + + throw error(500, { + message: 'Failed to fetch trip planning data', + error: err.message + }); + } +} From 6363b9a17e08a082b8013330ff46c508e674d0a3 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 22:07:22 +0200 Subject: [PATCH 04/47] docs: update README to clarify Geocoder API key requirements --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e36ce80..a9543ff 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,9 @@ See `.env.example` for an example of the required keys and values. ### Geocoding -- `PRIVATE_OBA_GEOCODER_API_KEY` - string: Your Geocoder service's API key. Leave this blank if you don't have one. +- `PRIVATE_OBA_GEOCODER_API_KEY` - string: Your Geocoder service's API key. Ensure that the Geocoder and Places API permissions are enabled. Leave this blank if you don't have one. - `PRIVATE_OBA_GEOCODER_PROVIDER` - string: Your Geocoder service. We currently only support the Google Places SDK (value: "google"). - ### Trip Planner - `PUBLIC_OTP_SERVER_URL` - string: Your OpenTripPlanner 1.x-compatible trip planner server URL. Leave this blank if you don't have one. From eada282ffc9f7d125578f7c1048dd8f0e3c9e234 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 22:53:00 +0200 Subject: [PATCH 05/47] feat: add Google Places autocomplete API integration --- .../oba/google-place-autocomplete/+server.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/routes/api/oba/google-place-autocomplete/+server.js diff --git a/src/routes/api/oba/google-place-autocomplete/+server.js b/src/routes/api/oba/google-place-autocomplete/+server.js new file mode 100644 index 0000000..8400bea --- /dev/null +++ b/src/routes/api/oba/google-place-autocomplete/+server.js @@ -0,0 +1,30 @@ +import { googlePlacesAutocomplete } from '$lib/geocoder'; + +import { + PRIVATE_OBA_GEOCODER_API_KEY as geocoderApiKey, + PRIVATE_OBA_GEOCODER_PROVIDER as geocoderProvider +} from '$env/static/private'; + +async function autoCompletePlacesSearch(input) { + if (geocoderProvider === 'google') { + return googlePlacesAutocomplete({ apiKey: geocoderApiKey, input }); + } else { + return []; + } +} + +export async function GET({ url }) { + const searchInput = url.searchParams.get('query')?.trim(); + + const suggestions = await autoCompletePlacesSearch(searchInput); + return new Response( + JSON.stringify({ + suggestions + }), + { + headers: { + 'Content-Type': 'application/json' + } + } + ); +} From d9bb7565d49d51e3d0233a1bc4601ef2c1bd1992 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 22:54:07 +0200 Subject: [PATCH 06/47] feat: implement Google Geocode location search API endpoint --- .../oba/google-geocode-location/+server.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/routes/api/oba/google-geocode-location/+server.js diff --git a/src/routes/api/oba/google-geocode-location/+server.js b/src/routes/api/oba/google-geocode-location/+server.js new file mode 100644 index 0000000..c43d41e --- /dev/null +++ b/src/routes/api/oba/google-geocode-location/+server.js @@ -0,0 +1,31 @@ +import { googleGeocode, googleGeocodeByPlaceId } from '$lib/geocoder'; + +import { + PRIVATE_OBA_GEOCODER_API_KEY as geocoderApiKey, + PRIVATE_OBA_GEOCODER_PROVIDER as geocoderProvider +} from '$env/static/private'; + +async function locationSearch(query) { + if (geocoderProvider === 'google') { + return googleGeocode({ apiKey: geocoderApiKey, query }); + } else { + return []; + } +} + +export async function GET({ url }) { + const searchInput = url.searchParams.get('query')?.trim(); + + const locationResponse = await locationSearch(searchInput); + + return new Response( + JSON.stringify({ + location: locationResponse + }), + { + headers: { + 'Content-Type': 'application/json' + } + } + ); +} From 0c67001f88bce1e506b294b76cf522633b1e6207 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 22:54:40 +0200 Subject: [PATCH 07/47] feat: add TripPlanModal integration for trip planning functionality --- src/routes/+page.svelte | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 329d666..1e5de1c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,6 +8,7 @@ import AlertsModal from '$components/navigation/AlertsModal.svelte'; import { onMount } from 'svelte'; import StopModal from '$components/stops/StopModal.svelte'; + import TripPlanModal from '$components/trip-planner/TripPlanModal.svelte'; let stop; let selectedTrip = null; @@ -15,6 +16,7 @@ let selectedRoute = null; let showRouteMap = false; let showAllRoutesModal = false; + let showTripPlanModal = false; let showRouteModal; let mapProvider = null; let currentIntervalId = null; @@ -23,6 +25,11 @@ let polylines = []; let stops = []; + let tripItineraries = []; + let loadingItineraries = false; + let fromMarker = null; + let toMarker = null; + $: { if (showRouteModal && showAllRoutesModal) { showAllRoutesModal = false; @@ -75,6 +82,7 @@ showAllRoutesModal = false; mapProvider.unHighlightMarker(currentHighlightedStopId); currentHighlightedStopId = null; + showTripPlanModal = false; } function tripSelected(event) { @@ -131,6 +139,17 @@ } } + function handleTripPlan(event) { + const tripData = event.detail.data; + fromMarker = event.detail.fromMarker; + toMarker = event.detail.toMarker; + tripItineraries = tripData.plan?.itineraries; + if (!tripItineraries) { + console.error('No itineraries found', 404); + } + showTripPlanModal = true; + } + onMount(() => { loadAlerts(); }); @@ -151,6 +170,7 @@ on:routeSelected={handleRouteSelected} on:clearResults={clearPolylines} on:viewAllRoutes={handleShowAllRoutes} + on:tripPlanned={handleTripPlan} />
@@ -173,6 +193,17 @@ on:routeSelected={handleRouteSelectedFromModal} /> {/if} + + {#if showTripPlanModal} + + {/if}
From 4e0ea2e6f6c89710c25c61b7983efa9b65aecdb5 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:22:55 +0200 Subject: [PATCH 08/47] feat: add pin marker functionality and enhance polyline creation options in OpenStreetMapProvider --- src/lib/Provider/OpenStreetMapProvider.js | 43 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/lib/Provider/OpenStreetMapProvider.js b/src/lib/Provider/OpenStreetMapProvider.js index f05b964..91747d5 100644 --- a/src/lib/Provider/OpenStreetMapProvider.js +++ b/src/lib/Provider/OpenStreetMapProvider.js @@ -7,6 +7,7 @@ import { COLORS } from '$lib/colors'; import PopupContent from '$components/map/PopupContent.svelte'; import { createVehicleIconSvg } from '$lib/MapHelpers/generateVehicleIcon'; import VehiclePopupContent from '$components/map/VehiclePopupContent.svelte'; +import TripPlanPinMarker from '$components/trip-planner/tripPlanPinMarker.svelte'; export default class OpenStreetMapProvider { constructor() { @@ -89,6 +90,38 @@ export default class OpenStreetMapProvider { return marker; } + addPinMarker(position, text) { + if (!this.map) return null; + + const container = document.createElement('div'); + + new TripPlanPinMarker({ + target: container, + props: { + text: text + } + }); + + const customIcon = this.L.divIcon({ + html: container, + className: '', + iconSize: [32, 50], + iconAnchor: [16, 50] + }); + + const marker = this.L.marker([position.lat, position.lng], { icon: customIcon }).addTo( + this.map + ); + + return marker; + } + + removePinMarker(marker) { + if (marker) { + marker.remove(); + } + } + highlightMarker(stopId) { const marker = this.markersMap.get(stopId); if (!marker) return; @@ -312,7 +345,7 @@ export default class OpenStreetMapProvider { }).addTo(this.map); } - createPolyline(points) { + createPolyline(points, options = { withArrow: true }) { if (!browser || !this.map) return null; const decodedPolyline = PolylineUtil.decode(points); @@ -322,11 +355,13 @@ export default class OpenStreetMapProvider { } const polyline = new this.L.Polyline(decodedPolyline, { - color: COLORS.POLYLINE, - weight: 4, - opacity: 1.0 + color: options.color || COLORS.POLYLINE, + weight: options.weight || 4, + opacity: options.opacity || 1 }).addTo(this.map); + if (!options.withArrow) return polyline; + const arrowDecorator = this.L.polylineDecorator(polyline, { patterns: [ { From 9221e738ac54713ee9756e30909010ff34b388e5 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:23:10 +0200 Subject: [PATCH 09/47] feat: Google Places autocomplete API function --- src/lib/geocoder.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/lib/geocoder.js b/src/lib/geocoder.js index ae3f072..8493da9 100644 --- a/src/lib/geocoder.js +++ b/src/lib/geocoder.js @@ -10,3 +10,17 @@ export async function googleGeocode({ apiKey, query }) { return null; } } + +export async function googlePlacesAutocomplete({ apiKey, input }) { + const response = await fetch(`https://places.googleapis.com/v1/places:autocomplete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Goog-Api-Key': apiKey + }, + body: JSON.stringify({ input }) + }); + const data = await response.json(); + + return data.suggestions; +} From 8e498855db82a087fee7103bbdd7b399aa4b5ab1 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:23:17 +0200 Subject: [PATCH 10/47] feat: add formatTime function for localized time formatting --- src/lib/formatters.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/formatters.js b/src/lib/formatters.js index c7e57bb..f11cfde 100644 --- a/src/lib/formatters.js +++ b/src/lib/formatters.js @@ -34,3 +34,11 @@ export function formatLastUpdated(timestamp, translations) { } return `${seconds} ${translations.sec} ${translations.ago}`; } + +export function formatTime(dateString) { + return new Date(dateString).toLocaleTimeString([], { + hour: 'numeric', + minute: '2-digit', + hour12: true + }); +} From fa25aa69169fb30bdc24f136b75b3f07ef63c372 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:52:15 +0200 Subject: [PATCH 11/47] feat: remove unused googleGeocodeByPlaceId import from geocoder --- src/routes/api/oba/google-geocode-location/+server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/api/oba/google-geocode-location/+server.js b/src/routes/api/oba/google-geocode-location/+server.js index c43d41e..27cc05c 100644 --- a/src/routes/api/oba/google-geocode-location/+server.js +++ b/src/routes/api/oba/google-geocode-location/+server.js @@ -1,4 +1,4 @@ -import { googleGeocode, googleGeocodeByPlaceId } from '$lib/geocoder'; +import { googleGeocode } from '$lib/geocoder'; import { PRIVATE_OBA_GEOCODER_API_KEY as geocoderApiKey, From 2f91f92fb0a776cd90ebc8893603604a331967cb Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:52:27 +0200 Subject: [PATCH 12/47] feat: close TripPlanModal when the tab is switched --- src/routes/+page.svelte | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1e5de1c..bbb2f96 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -9,6 +9,7 @@ import { onMount } from 'svelte'; import StopModal from '$components/stops/StopModal.svelte'; import TripPlanModal from '$components/trip-planner/TripPlanModal.svelte'; + import { browser } from '$app/environment'; let stop; let selectedTrip = null; @@ -152,6 +153,13 @@ onMount(() => { loadAlerts(); + + // close the trip plan modal when the tab is switched + if (browser) { + window.addEventListener('tabSwitched', () => { + showTripPlanModal = false; + }); + } }); From 1d1504915291955212e09dbd9ce30c33582c48fa Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:53:03 +0200 Subject: [PATCH 13/47] feat: add TripPlanSearchField component --- .../trip-planner/TripPlanSearchField.svelte | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/components/trip-planner/TripPlanSearchField.svelte diff --git a/src/components/trip-planner/TripPlanSearchField.svelte b/src/components/trip-planner/TripPlanSearchField.svelte new file mode 100644 index 0000000..a1d155a --- /dev/null +++ b/src/components/trip-planner/TripPlanSearchField.svelte @@ -0,0 +1,79 @@ + + +
+ +
+ + {#if place} + + {/if} +
+ {#if isLoading} +

+ Loading... +

+ {:else if results.length > 0} +
    + {#each results as result} + + {/each} +
+ {/if} +
From 24db030cd772b77fdad147ff1cf3f390bad4bd73 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:53:15 +0200 Subject: [PATCH 14/47] feat: add TripPlanPinMarker component --- .../trip-planner/tripPlanPinMarker.svelte | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/components/trip-planner/tripPlanPinMarker.svelte diff --git a/src/components/trip-planner/tripPlanPinMarker.svelte b/src/components/trip-planner/tripPlanPinMarker.svelte new file mode 100644 index 0000000..d3d0706 --- /dev/null +++ b/src/components/trip-planner/tripPlanPinMarker.svelte @@ -0,0 +1,31 @@ + + +
+
+ {text} +
+ + + +
From 40e50a624b6a2bcea77826f85e99bba9e99b530d Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:57:31 +0200 Subject: [PATCH 15/47] feat: add TripPlanModal component --- .../trip-planner/TripPlanModal.svelte | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/components/trip-planner/TripPlanModal.svelte diff --git a/src/components/trip-planner/TripPlanModal.svelte b/src/components/trip-planner/TripPlanModal.svelte new file mode 100644 index 0000000..cd6028e --- /dev/null +++ b/src/components/trip-planner/TripPlanModal.svelte @@ -0,0 +1,88 @@ + + + + {#if loading} + + {/if} + + {#if itineraries.length > 0} +
+ + {#each itineraries as _, index} + + {/each} +
+ +
+ {#if itineraries[activeTab]} + + {/if} +
+ {:else} +
+ No itineraries found +
+ {/if} +
From 2906d1f6eeab1d9e38b865b3f0f5f2736700ef6c Mon Sep 17 00:00:00 2001 From: Aaron Brethorst Date: Wed, 13 Nov 2024 08:28:06 -0800 Subject: [PATCH 16/47] feat: implement trip planner mood toggle in MapView component --- src/components/map/MapView.svelte | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/map/MapView.svelte b/src/components/map/MapView.svelte index e0910ee..f077f3f 100644 --- a/src/components/map/MapView.svelte +++ b/src/components/map/MapView.svelte @@ -19,6 +19,10 @@ export let showRouteMap = false; export let stop = null; export let mapProvider = null; +<<<<<<< HEAD +======= + let isTripPlanMoodActive = false; +>>>>>>> b21eed0 (feat: implement trip planner mood toggle in MapView component) let selectedStopID = null; let mapInstance = null; @@ -169,7 +173,18 @@ allStops.forEach((s) => addMarker(s)); } - function addMarker(s) { + // TODO: prevent fetch stops-for-location if the trip planner mood is on - we should do this after merge. + $: { + if (isTripPlanMoodActive) { + clearAllMarkers(); + } else { + if (!selectedRoute || !showRoute) { + allStops.forEach((s) => addMarker(s)); + } + } + } + + function addMarker(s, routeReference) { if (!mapInstance) { console.error('Map not initialized yet'); return; @@ -223,6 +238,12 @@ await initMap(); if (browser) { const darkMode = document.documentElement.classList.contains('dark'); + window.addEventListener('planTripTabClicked', () => { + isTripPlanMoodActive = true; + }); + window.addEventListener('tabSwitched', () => { + isTripPlanMoodActive = false; + }); const event = new CustomEvent('themeChange', { detail: { darkMode } }); window.dispatchEvent(event); } From d55e0223e81693c0a4ce963c31cbe78852b55577 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:58:28 +0200 Subject: [PATCH 17/47] feat: integrate TripPlan component --- src/components/search/SearchPane.svelte | 39 +++++++++++++++---------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/components/search/SearchPane.svelte b/src/components/search/SearchPane.svelte index 7371314..679fb5a 100644 --- a/src/components/search/SearchPane.svelte +++ b/src/components/search/SearchPane.svelte @@ -9,10 +9,13 @@ import { clearVehicleMarkersMap, fetchAndUpdateVehicles } from '$lib/vehicleUtils'; import { calculateMidpoint } from '$lib/mathUtils'; import { Tabs, TabItem } from 'flowbite-svelte'; + import { PUBLIC_OTP_SERVER_URL } from '$env/static/public'; + import TripPlan from '$components/trip-planner/TripPlan.svelte'; const dispatch = createEventDispatcher(); export let cssClasses = ''; + export let mapProvider = null; let routes = null; let stops = null; @@ -21,32 +24,24 @@ let polylines = []; let currentIntervalId = null; - export let mapProvider = null; - function handleLocationClick(location) { clearResults(); - const lat = location.geometry.location.lat; const lng = location.geometry.location.lng; - mapProvider.panTo(lat, lng); mapProvider.setZoom(20); - dispatch('locationSelected', { location }); } function handleStopClick(stop) { clearResults(); - mapProvider.panTo(stop.lat, stop.lon); mapProvider.setZoom(20); - dispatch('stopSelected', { stop }); } async function handleRouteClick(route) { clearResults(); - const response = await fetch(`/api/oba/stops-for-route/${route.id}`); const stopsForRoute = await response.json(); const stops = stopsForRoute.data.references.stops; @@ -55,7 +50,6 @@ for (const polylineData of polylinesData) { const shape = polylineData.points; let polyline; - polyline = mapProvider.createPolyline(shape); polylines.push(polyline); } @@ -63,10 +57,8 @@ await showStopsOnRoute(stops); currentIntervalId = await fetchAndUpdateVehicles(route.id, mapProvider); const midpoint = calculateMidpoint(stopsForRoute.data.references.stops); - mapProvider.panTo(midpoint.lat, midpoint.lng); mapProvider.setZoom(12); - dispatch('routeSelected', { route, stopsForRoute, stops, polylines, currentIntervalId }); } @@ -101,6 +93,20 @@ clearInterval(currentIntervalId); } + function handleTripPlan(event) { + dispatch('tripPlanned', event.detail); + } + + function handlePlanTripTabClick() { + const event = new CustomEvent('planTripTabClicked'); + window.dispatchEvent(event); + } + + function handleTabSwitch() { + const event = new CustomEvent('tabSwitched'); + window.dispatchEvent(event); + } + onMount(() => { window.addEventListener('routeSelectedFromModal', (event) => { handleRouteClick(event.detail.route); @@ -110,7 +116,7 @@
- + {#if query} @@ -168,8 +174,11 @@ >
- - plan a trip UI goes here! - + + {#if PUBLIC_OTP_SERVER_URL} + + + + {/if} From 88c3ed893803cc2692042483d9ea882bccb3807f Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:59:06 +0200 Subject: [PATCH 18/47] feat: add TripPlan component for trip planning functionality --- src/components/trip-planner/TripPlan.svelte | 197 ++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 src/components/trip-planner/TripPlan.svelte diff --git a/src/components/trip-planner/TripPlan.svelte b/src/components/trip-planner/TripPlan.svelte new file mode 100644 index 0000000..7ac0a95 --- /dev/null +++ b/src/components/trip-planner/TripPlan.svelte @@ -0,0 +1,197 @@ + + +
+ handleSearchInput(query, true)} + onClear={() => clearInput(true)} + onSelect={(location) => selectLocation(location, true)} + /> + + handleSearchInput(query, false)} + onClear={() => clearInput(false)} + onSelect={(location) => selectLocation(location, false)} + /> + + +
From 594a9d6e82e8f7adbf503ebf58c50d7154935795 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:59:30 +0200 Subject: [PATCH 19/47] feat: add LegDetails component for displaying trip leg information --- src/components/trip-planner/LegDetails.svelte | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/components/trip-planner/LegDetails.svelte diff --git a/src/components/trip-planner/LegDetails.svelte b/src/components/trip-planner/LegDetails.svelte new file mode 100644 index 0000000..e2379c8 --- /dev/null +++ b/src/components/trip-planner/LegDetails.svelte @@ -0,0 +1,98 @@ + + +
+
+ {#if icon} + + {/if} +
+ +
+

{modeText}

+

From: {leg.from.name}

+

To: {leg.to.name}

+

Distance: {Math.round(leg.distance)} meters

+

Duration: {Math.round(leg.duration / 60)} minutes

+

Start Time: {formatTime(leg.startTime)}

+

End Time: {formatTime(leg.endTime)}

+ + {#if leg.mode === 'WALK'} + + + {#if expandedSteps[index]} +
+ {#each leg.steps as step} +
+

+ {step.relativeDirection} on {step.streetName} +

+

Distance: {Math.round(step.distance)} meters

+

Direction: {step.absoluteDirection}

+
+ {/each} +
+ {/if} + {/if} +
+
From 88dc58bf3e524445c121b23cebe00fe30c31a0b2 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:59:43 +0200 Subject: [PATCH 20/47] feat: add ItineraryTab component for managing itinerary navigation --- src/components/trip-planner/ItineraryTab.svelte | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/components/trip-planner/ItineraryTab.svelte diff --git a/src/components/trip-planner/ItineraryTab.svelte b/src/components/trip-planner/ItineraryTab.svelte new file mode 100644 index 0000000..30272af --- /dev/null +++ b/src/components/trip-planner/ItineraryTab.svelte @@ -0,0 +1,14 @@ + + + From 28a13663ce41e35a21c8a3bc8d10ee22325ca5a9 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Mon, 11 Nov 2024 23:59:52 +0200 Subject: [PATCH 21/47] feat: add ItineraryDetails component for displaying itinerary information --- .../trip-planner/ItineraryDetails.svelte | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/components/trip-planner/ItineraryDetails.svelte diff --git a/src/components/trip-planner/ItineraryDetails.svelte b/src/components/trip-planner/ItineraryDetails.svelte new file mode 100644 index 0000000..ba57017 --- /dev/null +++ b/src/components/trip-planner/ItineraryDetails.svelte @@ -0,0 +1,19 @@ + + +
+

Duration: {Math.round(itinerary.duration / 60)} minutes

+

Start Time: {formatTime(itinerary.startTime)}

+

End Time: {formatTime(itinerary.endTime)}

+ +
+ {#each itinerary.legs as leg, index} + + {/each} +
+
From 998456f6256a9025848ca76e3ec2d130471b4878 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Tue, 12 Nov 2024 00:00:52 +0200 Subject: [PATCH 22/47] fix: update button color --- src/components/trip-planner/TripPlan.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/trip-planner/TripPlan.svelte b/src/components/trip-planner/TripPlan.svelte index 7ac0a95..a00a4ab 100644 --- a/src/components/trip-planner/TripPlan.svelte +++ b/src/components/trip-planner/TripPlan.svelte @@ -175,7 +175,7 @@ {/each} From 274d290d6afa6d339daf8a1080caddd5354134fb Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Tue, 12 Nov 2024 19:38:09 +0200 Subject: [PATCH 30/47] style: enhance layout and styling --- .../trip-planner/ItineraryDetails.svelte | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/trip-planner/ItineraryDetails.svelte b/src/components/trip-planner/ItineraryDetails.svelte index ba57017..8c15685 100644 --- a/src/components/trip-planner/ItineraryDetails.svelte +++ b/src/components/trip-planner/ItineraryDetails.svelte @@ -6,14 +6,22 @@ export let toggleSteps; -
-

Duration: {Math.round(itinerary.duration / 60)} minutes

-

Start Time: {formatTime(itinerary.startTime)}

-

End Time: {formatTime(itinerary.endTime)}

- -
- {#each itinerary.legs as leg, index} - - {/each} +
+
+

Duration

+

{Math.round(itinerary.duration / 60)} min

+
+
+

Start Time

+

{formatTime(itinerary.startTime)}

+
+

End Time

+

{formatTime(itinerary.endTime)}

+
+
+
+ {#each itinerary.legs as leg, index} + + {/each}
From 37b1028ad2ebfa49075245f19ddd7063cabdd2a5 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Tue, 12 Nov 2024 20:52:10 +0200 Subject: [PATCH 31/47] feat: enhance LegDetails component with additional icons and improved layout --- src/components/trip-planner/LegDetails.svelte | 95 +++++++++++++------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/src/components/trip-planner/LegDetails.svelte b/src/components/trip-planner/LegDetails.svelte index e2379c8..4b55117 100644 --- a/src/components/trip-planner/LegDetails.svelte +++ b/src/components/trip-planner/LegDetails.svelte @@ -7,7 +7,12 @@ faChevronDown, faChevronUp, faFerry, - faTrainSubway + faTrainSubway, + faMapMarkerAlt, + faRulerCombined, + faClock, + faArrowRight, + faArrowAltCircleRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome'; @@ -15,15 +20,12 @@ export let index; export let expandedSteps; export let toggleSteps; - - // TODO: ADD ICONS FOR OTHER MODES - let icon; - let modeText; - let iconColor; + let icon, modeText, iconColor; + let isWalking = leg.mode === 'WALK'; switch (leg.mode) { case 'WALK': icon = faWalking; - modeText = 'Walking'; + modeText = 'Walk'; iconColor = 'text-blue-600'; break; case 'BUS': @@ -32,11 +34,6 @@ iconColor = 'text-green-600'; break; case 'TRAIN': - icon = faTrain; - modeText = `Train - ${leg.route}`; - iconColor = 'text-red-600'; - break; - case 'RAIL': icon = faTrain; modeText = `Train - ${leg.route}`; @@ -58,37 +55,73 @@ } -
-
+
+
+ +
{#if icon} - + {/if}
-
-

{modeText}

-

From: {leg.from.name}

-

To: {leg.to.name}

-

Distance: {Math.round(leg.distance)} meters

-

Duration: {Math.round(leg.duration / 60)} minutes

-

Start Time: {formatTime(leg.startTime)}

-

End Time: {formatTime(leg.endTime)}

+
+
+
{leg.from.name}
+
+ +
+
+ + Start: + {formatTime(leg.startTime)} +
+
+ + End: {formatTime(leg.endTime)} +
+
+ +
+
+ + {leg.to.name} +
+
+ + Distance: {Math.round(leg.distance)} meters +
+
+ + Duration: {Math.round(leg.duration / 60)} minutes +
+
- {#if leg.mode === 'WALK'} - {#if expandedSteps[index]} -
+
{#each leg.steps as step}
-

- {step.relativeDirection} on {step.streetName} -

-

Distance: {Math.round(step.distance)} meters

-

Direction: {step.absoluteDirection}

+
{step.relativeDirection} on {step.streetName}
+
+ + Distance: {Math.round(step.distance)} meters +
+
+ + {step.absoluteDirection} +
{/each}
From 0ba40c425f4d7b00bba3bfe3eceebf5a16fab451 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Tue, 12 Nov 2024 23:59:38 +0200 Subject: [PATCH 32/47] feat: add pin marker functionality to GoogleMapProvider --- src/lib/Provider/GoogleMapProvider.js | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/lib/Provider/GoogleMapProvider.js b/src/lib/Provider/GoogleMapProvider.js index 1781a3a..9ab944d 100644 --- a/src/lib/Provider/GoogleMapProvider.js +++ b/src/lib/Provider/GoogleMapProvider.js @@ -5,6 +5,7 @@ import { COLORS } from '$lib/colors'; import PopupContent from '$components/map/PopupContent.svelte'; import VehiclePopupContent from '$components/map/VehiclePopupContent.svelte'; import { createVehicleIconSvg } from '$lib/MapHelpers/generateVehicleIcon'; +import TripPlanPinMarker from '$components/trip-planner/tripPlanPinMarker.svelte'; export default class GoogleMapProvider { constructor(apiKey) { this.apiKey = apiKey; @@ -154,6 +155,10 @@ export default class GoogleMapProvider { unHighlightMarker(stopId) { const marker = this.markersMap.get(stopId); + + if (!marker) { + return; + } marker.$set({ isHighlighted: false }); } @@ -164,6 +169,57 @@ export default class GoogleMapProvider { this.stopMarkers = []; } + addPinMarker(position, text) { + const container = document.createElement('div'); + document.body.appendChild(container); + + new TripPlanPinMarker({ + target: container, + props: { + text: text + } + }); + + const overlay = new google.maps.OverlayView(); + + overlay.onAdd = function () { + this.getPanes().overlayMouseTarget.appendChild(container); + }; + + overlay.draw = function () { + const projection = this.getProjection(); + const pos = projection.fromLatLngToDivPixel( + new google.maps.LatLng(position.lat, position.lng) + ); + container.style.left = `${pos.x - 16}px`; + container.style.top = `${pos.y - 50}px`; + container.style.position = 'absolute'; + container.style.zIndex = '1000'; + }; + + overlay.onRemove = function () { + container.parentNode.removeChild(container); + }; + + overlay.setMap(this.map); + + return { overlay, element: container }; + } + + removePinMarker(marker) { + if (!marker) { + return; + } + + if (marker.overlay) { + marker.overlay.setMap(null); + } + + if (marker.element && marker.element.parentNode) { + marker.element.parentNode.removeChild(marker.element); + } + } + addVehicleMarker(vehicle, activeTrip) { if (!this.map) return null; From 0c7b65c7ba39ad5199de709e8026ab223eee9917 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Tue, 12 Nov 2024 23:59:47 +0200 Subject: [PATCH 33/47] fix: ensure polyline removal handles asynchronous operations in TripPlanModal --- src/components/trip-planner/TripPlanModal.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/trip-planner/TripPlanModal.svelte b/src/components/trip-planner/TripPlanModal.svelte index cd6028e..47d4d0c 100644 --- a/src/components/trip-planner/TripPlanModal.svelte +++ b/src/components/trip-planner/TripPlanModal.svelte @@ -55,8 +55,8 @@ mapProvider.removePinMarker(toMarker); if (currPolylines.length > 0) { - currPolylines.forEach((polyline) => { - mapProvider.removePolyline(polyline); + currPolylines.forEach(async (polyline) => { + mapProvider.removePolyline(await polyline); }); } }); From b2d91a167ea494762d516452466c4aac675eb816 Mon Sep 17 00:00:00 2001 From: Ahmedhossamdev Date: Wed, 13 Nov 2024 00:00:03 +0200 Subject: [PATCH 34/47] style: improve button styling --- src/components/trip-planner/TripPlan.svelte | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/trip-planner/TripPlan.svelte b/src/components/trip-planner/TripPlan.svelte index 191304d..ddf75af 100644 --- a/src/components/trip-planner/TripPlan.svelte +++ b/src/components/trip-planner/TripPlan.svelte @@ -175,12 +175,20 @@