Skip to content

Commit b720e87

Browse files
Merge pull request #186 from OneBusAway/feat/add-bing-geocoder
Feat/add bing geocoder
2 parents 6368f95 + edef5fb commit b720e87

File tree

8 files changed

+451
-34
lines changed

8 files changed

+451
-34
lines changed

src/components/search/SearchPane.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
on:click={() => handleLocationClick(location)}
142142
title={location.formatted_address}
143143
icon={faMapPin}
144-
subtitle={location.types.join(', ')}
144+
subtitle={location?.types?.join(', ') || location.name}
145145
/>
146146
{/if}
147147

src/components/trip-planner/TripPlan.svelte

+10-13
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,14 @@
2121
let lockSelectLocation = false;
2222
2323
async function fetchAutocompleteResults(query) {
24-
const response = await fetch(
25-
`/api/oba/google-place-autocomplete?query=${encodeURIComponent(query)}`
26-
);
24+
const response = await fetch(`/api/oba/place-suggestions?query=${encodeURIComponent(query)}`);
25+
26+
if (!response.ok) {
27+
throw error('Error fetching location results', 500);
28+
}
2729
const data = await response.json();
2830
29-
return data.suggestions
30-
? data.suggestions.map((suggestion) => ({
31-
placeId: suggestion.placePrediction.placeId,
32-
text: suggestion.placePrediction.text.text
33-
}))
34-
: [];
31+
return data.suggestions;
3532
}
3633
3734
const fetchLocationResults = debounce(async (query, isFrom) => {
@@ -52,7 +49,7 @@
5249
5350
async function geocodeLocation(locationName) {
5451
const response = await fetch(
55-
`/api/oba/google-geocode-location?query=${encodeURIComponent(locationName)}`
52+
`/api/oba/geocode-location?query=${encodeURIComponent(locationName)}`
5653
);
5754
5855
if (!response.ok) {
@@ -77,16 +74,16 @@
7774
if (lockSelectLocation) return;
7875
lockSelectLocation = true;
7976
try {
80-
const response = await geocodeLocation(suggestion.text);
77+
const response = await geocodeLocation(suggestion.name);
8178
if (isFrom) {
8279
selectedFrom = response.location.geometry.location;
8380
fromMarker = mapProvider.addPinMarker(selectedFrom, $t('trip-planner.from'));
84-
fromPlace = suggestion.text;
81+
fromPlace = suggestion.name;
8582
fromResults = [];
8683
} else {
8784
selectedTo = response.location.geometry.location;
8885
toMarker = mapProvider.addPinMarker(selectedTo, $t('trip-planner.to'));
89-
toPlace = suggestion.text;
86+
toPlace = suggestion.name;
9087
toResults = [];
9188
}
9289
} catch (error) {

src/components/trip-planner/TripPlanSearchField.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
onclick={() => handleSelect(result)}
8989
>
9090
<FontAwesomeIcon icon={faMapMarkerAlt} class="mr-2 text-gray-400 " />
91-
{result.text}
91+
{result.displayText}
9292
</button>
9393
{/each}
9494
</ul>

src/lib/geocoder.js

+163-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ export async function googleGeocode({ apiKey, query }) {
44
);
55
const data = await response.json();
66

7-
if (data.status === 'OK') {
8-
return data.results[0];
9-
} else {
7+
if (data.status !== 'OK' || data.results.length === 0) {
108
return null;
119
}
10+
11+
const result = data.results[0];
12+
13+
return createGeocodingResult({
14+
geometry: result.geometry,
15+
formatted_address: result.formatted_address,
16+
name: result.formatted_address
17+
});
1218
}
1319

1420
export async function googlePlacesAutocomplete({ apiKey, input }) {
@@ -22,5 +28,158 @@ export async function googlePlacesAutocomplete({ apiKey, input }) {
2228
});
2329
const data = await response.json();
2430

25-
return data.suggestions;
31+
if (!data.suggestions) {
32+
return [];
33+
}
34+
35+
const suggestions = [];
36+
for (const suggestion of data.suggestions) {
37+
const prediction = suggestion.placePrediction;
38+
39+
const suggestionObject = createSuggestion(
40+
prediction.placeId,
41+
prediction.text.text,
42+
prediction.text.text
43+
);
44+
45+
if (suggestionObject) {
46+
suggestions.push(suggestionObject);
47+
}
48+
}
49+
50+
return suggestions;
51+
}
52+
53+
export async function bingGeocode({ apiKey, query }) {
54+
const rawBingResult = await fetch(
55+
`https://dev.virtualearth.net/REST/v1/Locations?query=${encodeURIComponent(query)}&key=${apiKey}`,
56+
{
57+
method: 'GET',
58+
headers: { Accept: 'application/json' }
59+
}
60+
);
61+
62+
const data = await rawBingResult.json();
63+
64+
if (data.resourceSets[0].estimatedTotal === 0) {
65+
return null;
66+
}
67+
68+
const resource = data.resourceSets[0].resources[0];
69+
70+
return createGeocodingResult({
71+
geometry: {
72+
location: {
73+
lat: resource.point.coordinates[0] ?? null,
74+
lng: resource.point.coordinates[1] ?? null
75+
}
76+
},
77+
formatted_address: resource.address.formattedAddress,
78+
name: resource.name ?? '',
79+
placeId: null
80+
});
81+
}
82+
83+
export async function bingAutoSuggestPlaces({ apiKey, query }) {
84+
const rawBingResult = await fetch(
85+
`https://dev.virtualearth.net/REST/v1/Autosuggest?query=${encodeURIComponent(query)}&key=${apiKey}`,
86+
{
87+
method: 'GET',
88+
headers: { Accept: 'application/json' }
89+
}
90+
);
91+
92+
const data = await rawBingResult.json();
93+
94+
const resourceSets = data.resourceSets;
95+
96+
if (!resourceSets || resourceSets.length === 0 || resourceSets[0].estimatedTotal === 0) {
97+
return [];
98+
}
99+
100+
const resources = resourceSets[0].resources;
101+
if (!resources || resources.length === 0) {
102+
return [];
103+
}
104+
105+
const suggestions = [];
106+
for (const resource of resources) {
107+
if (resource.value && Array.isArray(resource.value)) {
108+
for (const item of resource.value) {
109+
const displayText = item.name
110+
? `${item.name} - ${item.address.formattedAddress}`
111+
: item.address.formattedAddress;
112+
113+
const suggestion = createSuggestion(
114+
null,
115+
item.name || item.address.formattedAddress,
116+
displayText
117+
);
118+
119+
if (suggestion) {
120+
suggestions.push(suggestion);
121+
}
122+
}
123+
} else {
124+
const suggestion = createSuggestion(
125+
null,
126+
resource.name || resource.address.formattedAddress,
127+
resource.address.formattedAddress
128+
);
129+
130+
if (suggestion) {
131+
suggestions.push(suggestion);
132+
}
133+
}
134+
}
135+
136+
return suggestions;
137+
}
138+
139+
export async function fetchAutocompleteResults(provider, query, apiKey) {
140+
switch (provider) {
141+
case 'google':
142+
return await googlePlacesAutocomplete({ apiKey, input: query });
143+
case 'bing':
144+
return await bingAutoSuggestPlaces({ apiKey, query });
145+
default:
146+
throw new Error('Invalid geocoding provider');
147+
}
148+
}
149+
150+
/**
151+
*
152+
* @param {string} placeId optional - some providers return a placeId
153+
* @param {string} name required - used for geocoding the selected place
154+
* @param {string} displayText required - used for displaying the selected place
155+
* @returns
156+
*/
157+
function createSuggestion(placeId, name, displayText) {
158+
if (!name || !displayText) return null;
159+
160+
return {
161+
...(placeId && { placeId }),
162+
name,
163+
displayText
164+
};
165+
}
166+
167+
/**
168+
*
169+
* @param {location{lat,lng}} geometry
170+
* @param {string} formatted_address
171+
* @param {string} name
172+
* @returns
173+
*/
174+
function createGeocodingResult({ geometry, formatted_address, name }) {
175+
return {
176+
name: name || formatted_address,
177+
formatted_address: formatted_address,
178+
geometry: {
179+
location: {
180+
lat: geometry.location.lat,
181+
lng: geometry.location.lng
182+
}
183+
}
184+
};
26185
}

src/routes/api/oba/google-geocode-location/+server.js renamed to src/routes/api/oba/geocode-location/+server.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { googleGeocode } from '$lib/geocoder';
1+
import { bingGeocode, googleGeocode } from '$lib/geocoder';
22

33
import { PRIVATE_OBA_GEOCODER_PROVIDER as geocoderProvider } from '$env/static/private';
44

@@ -10,7 +10,7 @@ async function locationSearch(query) {
1010
if (geocoderProvider === 'google') {
1111
return googleGeocode({ apiKey: geocoderApiKey, query });
1212
} else {
13-
return [];
13+
return bingGeocode({ apiKey: geocoderApiKey, query });
1414
}
1515
}
1616

src/routes/api/oba/google-place-autocomplete/+server.js renamed to src/routes/api/oba/place-suggestions/+server.js

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
1-
import { googlePlacesAutocomplete } from '$lib/geocoder';
1+
import { fetchAutocompleteResults } from '$lib/geocoder';
22

33
import { PRIVATE_OBA_GEOCODER_PROVIDER as geocoderProvider } from '$env/static/private';
44

55
import { env } from '$env/dynamic/private';
66

77
let geocoderApiKey = env.PRIVATE_OBA_GEOCODER_API_KEY;
88

9-
async function autoCompletePlacesSearch(input) {
10-
if (geocoderProvider === 'google') {
11-
return googlePlacesAutocomplete({ apiKey: geocoderApiKey, input });
12-
} else {
13-
return [];
14-
}
15-
}
16-
179
export async function GET({ url }) {
1810
const searchInput = url.searchParams.get('query')?.trim();
1911

20-
const suggestions = await autoCompletePlacesSearch(searchInput);
12+
const suggestions = await fetchAutocompleteResults(geocoderProvider, searchInput, geocoderApiKey);
13+
2114
return new Response(
2215
JSON.stringify({
2316
suggestions

src/routes/api/oba/search/+server.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OnebusawaySDK } from 'onebusaway-sdk';
22

3-
import { googleGeocode } from '$lib/geocoder';
3+
import { bingGeocode, googleGeocode } from '$lib/geocoder';
44

55
import {
66
PUBLIC_OBA_SERVER_URL as baseUrl,
@@ -43,8 +43,8 @@ async function stopSearch(query) {
4343
async function locationSearch(query) {
4444
if (geocoderProvider === 'google') {
4545
return googleGeocode({ apiKey: geocoderApiKey, query });
46-
} else {
47-
return [];
46+
} else if (geocoderProvider === 'bing') {
47+
return bingGeocode({ apiKey: geocoderApiKey, query });
4848
}
4949
}
5050

0 commit comments

Comments
 (0)