Skip to content

Commit eade813

Browse files
committed
feat(geocoder): add support for geocoding without a geocoding API
1 parent 961a57f commit eade813

File tree

6 files changed

+120
-27
lines changed

6 files changed

+120
-27
lines changed

__tests__/util/__snapshots__/geocoder.js.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,34 @@ Object {
9393
}
9494
`;
9595

96+
exports[`geocoder NoApiGeocoder should get location from geocode feature 1`] = `
97+
Object {
98+
"lat": 45.516198,
99+
"lon": -122.67324,
100+
"name": "45.516198, -122.673240",
101+
}
102+
`;
103+
104+
exports[`geocoder NoApiGeocoder should make autocomplete query 1`] = `
105+
Object {
106+
"features": Array [],
107+
}
108+
`;
109+
110+
exports[`geocoder NoApiGeocoder should make reverse query 1`] = `
111+
Object {
112+
"lat": 45.5162,
113+
"lon": -122.67324,
114+
"name": "45.5162, -122.67324",
115+
}
116+
`;
117+
118+
exports[`geocoder NoApiGeocoder should make search query 1`] = `
119+
Object {
120+
"features": Array [],
121+
}
122+
`;
123+
96124
exports[`geocoder PELIAS should get location from geocode feature 1`] = `
97125
Object {
98126
"lat": 45.516198,

__tests__/util/geocoder.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ function mockResponsePath (geocoder, file) {
99
describe('geocoder', () => {
1010
const geocoders = [
1111
{
12+
type: 'ARCGIS'
13+
}, {
1214
apiKey: 'dummy-mapzen-key',
1315
baseUrl: 'https://ws-st.trimet.org/pelias/v1',
1416
type: 'PELIAS'
15-
}, {
16-
type: 'ARCGIS'
17-
}
17+
},
18+
// this entry represents no geocoder configuration. In this case it is
19+
// expected that the NoApiGeocoder will be used.
20+
undefined
1821
]
1922

2023
// nocks for ARCGIS
@@ -54,8 +57,11 @@ describe('geocoder', () => {
5457
.replyWithFile(200, mockResponsePath('pelias', 'reverse-response.json'))
5558

5659
geocoders.forEach(geocoder => {
60+
const geocoderType = geocoder
61+
? geocoder.type
62+
: 'NoApiGeocoder'
5763
// the describe is in quotes to bypass a lint rule
58-
describe(`${geocoder.type}`, () => {
64+
describe(`${geocoderType}`, () => {
5965
it('should make autocomplete query', async () => {
6066
const result = await getGeocoder(geocoder).autocomplete({ text: 'Mill Ends' })
6167
expect(result).toMatchSnapshot()
@@ -74,7 +80,7 @@ describe('geocoder', () => {
7480

7581
it('should get location from geocode feature', async () => {
7682
let mockFeature
77-
switch (geocoder.type) {
83+
switch (geocoderType) {
7884
case 'ARCGIS':
7985
mockFeature = {
8086
magicKey: 'abcd',
@@ -95,6 +101,17 @@ describe('geocoder', () => {
95101
}
96102
}
97103
break
104+
case 'NoApiGeocoder':
105+
mockFeature = {
106+
geometry: {
107+
coordinates: [-122.673240, 45.516198],
108+
type: 'Point'
109+
},
110+
properties: {
111+
label: '45.516198, -122.673240'
112+
}
113+
}
114+
break
98115
default:
99116
throw new Error(`no mock feature defined for geocoder type: ${geocoder.type}`)
100117
}

example-config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ map:
3737
url: http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png
3838
attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
3939

40+
# it is possible to leave out a geocoder config entirely. In that case only
41+
# GPS coordinates will be used when finding the origin/destination.
42+
4043
# example config for a Pelias geocoder (https://pelias.io/)
4144
geocoder:
4245
apiKey: MAPZEN_KEY

lib/reducers/create-otp-reducer.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,6 @@ const MAX_RECENT_STORAGE = 5
5151

5252
// TODO: fire planTrip action if default query is complete/error-free
5353

54-
// a list of required keys that must be present in the initial config of the
55-
// initial state
56-
const requiredConfigKeys = [
57-
'geocoder.type'
58-
]
59-
6054
/**
6155
* Validates the initial state of the store. This is intended to mainly catch
6256
* configuration issues since a manually edited config file is loaded into the
@@ -69,11 +63,6 @@ function validateInitalState (initialState) {
6963

7064
const errors = []
7165

72-
// iterate through required keys to ensure integrity of config
73-
requiredConfigKeys.forEach(k => {
74-
if (!objectPath.has(config, k)) errors.push(new Error(`Config is missing required key: "${k}"`))
75-
})
76-
7766
// validate that the ArcGIS geocoder isn't used with a persistence strategy of
7867
// `localStorage`. ArcGIS requires the use of a paid account to store geocode
7968
// results.

lib/util/geocoder.js

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ class Geocoder {
4040
* done to obtain that detailed data.
4141
*/
4242
getLocationFromGeocodedFeature (feature) {
43-
return Promise.resolve(feature)
43+
const location = lonlat.fromCoordinates(feature.geometry.coordinates)
44+
location.name = feature.properties.label
45+
return Promise.resolve(location)
4446
}
4547

4648
/**
@@ -147,21 +149,71 @@ class ArcGISGeocoder extends Geocoder {
147149
}
148150

149151
/**
150-
* Geocoder implementation for the Pelias geocoder.
151-
* See https://pelias.io
152+
* An implementation that doesn't use an API for geocoding. Merely allows
153+
* clicking on the map and finding GPS coordinates by typing them in.
152154
*
153155
* @extends Geocoder
154156
*/
155-
class PeliasGeocoder extends Geocoder {
157+
class NoApiGeocoder extends Geocoder {
156158
/**
157-
* Translate the given feature into an application-specific data format.
159+
* Use coordinate string parser.
158160
*/
159-
getLocationFromGeocodedFeature (feature) {
160-
const location = lonlat.fromCoordinates(feature.geometry.coordinates)
161-
location.name = feature.properties.label
162-
return Promise.resolve(location)
161+
autocomplete (query) {
162+
return this._parseCoordinateString(query.text)
163163
}
164164

165+
/**
166+
* Always return the lat/lon.
167+
*/
168+
reverse (query) {
169+
let { lat, lon } = query.point
170+
lat = this._roundGPSDecimal(lat)
171+
lon = this._roundGPSDecimal(lon)
172+
return Promise.resolve({ lat, lon, name: `${lat}, ${lon}` })
173+
}
174+
175+
/**
176+
* Use coordinate string parser.
177+
*/
178+
search (query) {
179+
return this._parseCoordinateString(query.text)
180+
}
181+
182+
/**
183+
* Attempt to parse the input as a GPS coordinate. If parseable, return a
184+
* feature.
185+
*/
186+
_parseCoordinateString (string) {
187+
let feature
188+
try {
189+
feature = {
190+
geometry: {
191+
coordinates: lonlat.toCoordinates(lonlat.fromLatFirstString(string)),
192+
type: 'Point'
193+
},
194+
properties: {
195+
label: string
196+
}
197+
}
198+
} catch (e) {
199+
return Promise.resolve({ features: [] })
200+
}
201+
return Promise.resolve({ features: [feature] })
202+
}
203+
204+
_roundGPSDecimal (number) {
205+
const roundFactor = 100000
206+
return Math.round(number * roundFactor) / roundFactor
207+
}
208+
}
209+
210+
/**
211+
* Geocoder implementation for the Pelias geocoder.
212+
* See https://pelias.io
213+
*
214+
* @extends Geocoder
215+
*/
216+
class PeliasGeocoder extends Geocoder {
165217
/**
166218
* Rewrite the response into an application-specific data format using the
167219
* first feature returned from the geocoder.
@@ -178,14 +230,18 @@ class PeliasGeocoder extends Geocoder {
178230

179231
// Create a memoized getter to avoid recreating new geocoders each time.
180232
const getGeocoder = memoize(geocoderConfig => {
233+
if (!geocoderConfig || !geocoderConfig.type) {
234+
return new NoApiGeocoder()
235+
}
181236
const {type} = geocoderConfig
182237
switch (type) {
183238
case 'ARCGIS':
184239
return new ArcGISGeocoder(arcgis, geocoderConfig)
185240
case 'PELIAS':
186241
return new PeliasGeocoder(pelias, geocoderConfig)
187242
default:
188-
throw new Error(`Invalid geocoder type: ${type}`)
243+
console.error(`Unkown geocoder type: "${type}". Using NoApiGeocoder.`)
244+
return new NoApiGeocoder()
189245
}
190246
})
191247

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"prepublish": "mastarm prepublish --config configurations/prepublish",
1212
"prestart": "yarn",
1313
"test": "yarn run lint && yarn run lint-docs && yarn run jest",
14-
"start": "mastarm build --serve example.js"
14+
"start": "mastarm build -e development --serve example.js"
1515
},
1616
"standard": {
1717
"parser": "babel-eslint"

0 commit comments

Comments
 (0)