Commit 6d8f59b5 authored by JOE XMG's avatar JOE XMG
Browse files

update

parent 22fcb4d1
Pipeline #6293 passed with stage
in 6 seconds
import { testXMLHttpRequest } from './mockXMLHttpRequest';
import { Google } from '../src/geocoders/google';
import { GeocodingResult } from '../src/geocoders/api';
describe('L.Control.Geocoder.Google', () => {
it('geocodes Innsbruck', () => {
const geocoder = new Google({ apiKey: '0123xyz' });
const callback = jest.fn();
testXMLHttpRequest(
'https://maps.googleapis.com/maps/api/geocode/json?key=0123xyz&address=Innsbruck',
{
results: [
{
address_components: [
{
long_name: 'Innsbruck',
short_name: 'Innsbruck',
types: ['locality', 'political']
},
{
long_name: 'Innsbruck',
short_name: 'Innsbruck',
types: ['administrative_area_level_2', 'political']
},
{
long_name: 'Tyrol',
short_name: 'Tyrol',
types: ['administrative_area_level_1', 'political']
},
{
long_name: 'Austria',
short_name: 'AT',
types: ['country', 'political']
}
],
formatted_address: 'Innsbruck, Austria',
geometry: {
bounds: {
northeast: {
lat: 47.3599301,
lng: 11.45593
},
southwest: {
lat: 47.21098000000001,
lng: 11.3016499
}
},
location: {
lat: 47.2692124,
lng: 11.4041024
},
location_type: 'APPROXIMATE',
viewport: {
northeast: {
lat: 47.3599301,
lng: 11.45593
},
southwest: {
lat: 47.21098000000001,
lng: 11.3016499
}
}
},
place_id: 'ChIJc8r44c9unUcRDZsdKH0cIJ0',
types: ['locality', 'political']
}
],
status: 'OK'
},
() => geocoder.geocode('Innsbruck', callback)
);
const feature: GeocodingResult = callback.mock.calls[0][0][0];
expect(feature.name).toBe('Innsbruck, Austria');
expect(feature.center).toStrictEqual({ lat: 47.2692124, lng: 11.4041024 });
expect(feature.bbox).toStrictEqual({
_northEast: { lat: 47.3599301, lng: 11.45593 },
_southWest: { lat: 47.21098000000001, lng: 11.3016499 }
});
expect(callback.mock.calls).toMatchSnapshot();
});
});
import { testXMLHttpRequest } from './mockXMLHttpRequest';
import { HERE } from '../src/geocoders/here';
import { HEREv2 } from '../src/geocoders/here';
import { GeocodingResult } from '../src/geocoders/api';
describe('L.Control.Geocoder.HERE', () => {
it('geocodes Innsbruck', () => {
const geocoder = new HERE({ app_id: 'xxx', app_code: 'yyy' });
const callback = jest.fn();
testXMLHttpRequest(
'https://geocoder.api.here.com/6.2/geocode.json?searchtext=Innsbruck&gen=9&app_id=xxx&app_code=yyy&jsonattributes=1&maxresults=5',
{
response: {
view: [
{
result: [
{
relevance: 1,
matchLevel: 'city',
matchQuality: {
city: 1
},
location: {
locationId: 'NT_Q9dJCLiAU-LWKKq1nkKnGD',
locationType: 'area',
displayPosition: {
latitude: 47.268,
longitude: 11.3913
},
navigationPosition: [
{
latitude: 47.268,
longitude: 11.3913
}
],
mapView: {
topLeft: {
latitude: 47.35922,
longitude: 11.30194
},
bottomRight: {
latitude: 47.21082,
longitude: 11.45587
}
},
address: {
label: 'Innsbruck, Tirol, Österreich',
country: 'AUT',
state: 'Tirol',
county: 'Innsbruck-Stadt',
city: 'Innsbruck',
postalCode: '6020',
additionalData: [
{
value: 'Österreich',
key: 'CountryName'
},
{
value: 'Tirol',
key: 'StateName'
},
{
value: 'Innsbruck-Stadt',
key: 'CountyName'
}
]
}
}
}
],
viewId: 0
}
]
}
},
() => geocoder.geocode('Innsbruck', callback)
);
const feature: GeocodingResult = callback.mock.calls[0][0][0];
expect(feature.name).toBe('Innsbruck, Tirol, Österreich');
expect(feature.center).toStrictEqual({ lat: 47.268, lng: 11.3913 });
expect(feature.bbox).toStrictEqual({
_northEast: { lat: 47.35922, lng: 11.45587 },
_southWest: { lat: 47.21082, lng: 11.30194 }
});
expect(callback.mock.calls).toMatchSnapshot();
});
});
describe('L.Control.Geocoder.HEREv2', () => {
it('geocodes Innsbruck', () => {
const geocodingParams = { at: '50.62925,3.057256' };
const geocoder = new HEREv2({ apiKey: 'xxx', geocodingQueryParams: geocodingParams });
const callback = jest.fn();
testXMLHttpRequest(
'https://geocode.search.hereapi.com/v1/discover?q=Innsbruck&apiKey=xxx&limit=10&at=50.62925%2C3.057256',
{
items: [
{
title: 'Salumeria Italiana',
id: 'here:pds:place:840drt3p-898f6ee434794fe59895e71ccf9381e1',
ontologyId: 'here:cm:ontology:restaurant',
resultType: 'place',
address: {
label: 'Salumeria Italiana, 151 Richmond St, Boston, MA 02109, United States',
countryCode: 'USA',
countryName: 'United States',
stateCode: 'MA',
state: 'Massachusetts',
county: 'Suffolk',
city: 'Boston',
district: 'North End',
street: 'Richmond St',
postalCode: '02109',
houseNumber: '151'
},
position: { lat: 42.36355, lng: -71.05439 },
access: [{ lat: 42.3635, lng: -71.05448 }],
distance: 11,
categories: [
{ id: '600-6300-0066', name: 'Grocery', primary: true },
{ id: '100-1000-0000', name: 'Restaurant' },
{ id: '100-1000-0006', name: 'Deli' },
{ id: '600-6300-0067', name: 'Specialty Food Store' }
],
references: [
{ supplier: { id: 'core' }, id: '31213861' },
{ supplier: { id: 'tripadvisor' }, id: '3172680' },
{ supplier: { id: 'yelp' }, id: 'JNx0DlfndRurT-8KhSym7g' },
{ supplier: { id: 'yelp' }, id: 'P44VNcZUUNZfiFy-c4SUJw' }
],
foodTypes: [
{ id: '304-000', name: 'Italian', primary: true },
{ id: '800-057', name: 'Pizza' },
{ id: '800-060', name: 'Sandwich' }
],
contacts: [
{
phone: [
{ value: '+16175234946' },
{ value: '+16175238743' },
{ value: '+16177204243' }
],
fax: [{ value: '+16175234946' }],
www: [{ value: 'http://www.salumeriaitaliana.com' }],
email: [{ value: 'contact@salumeriaitaliana.com' }]
}
],
openingHours: [
{
text: ['Mon-Sat: 08:00 - 17:00', 'Sun: 10:00 - 16:00'],
isOpen: false,
structured: [
{
start: 'T080000',
duration: 'PT11H00M',
recurrence: 'FREQ:DAILY;BYDAY:MO,TU,WE,TH,FR,SA'
},
{ start: 'T100000', duration: 'PT06H00M', recurrence: 'FREQ:DAILY;BYDAY:SU' }
]
}
]
}
]
},
() => geocoder.geocode('Innsbruck', callback)
);
const feature: GeocodingResult = callback.mock.calls[0][0][0];
expect(feature.name).toBe(
'Salumeria Italiana, 151 Richmond St, Boston, MA 02109, United States'
);
expect(feature.center).toStrictEqual({ lat: 42.36355, lng: -71.05439 });
expect(callback.mock.calls).toMatchSnapshot();
});
});
import * as L from 'leaflet';
import { LatLng } from '../src/geocoders/latlng';
describe('LatLng', () => {
// test cases from https://github.com/openstreetmap/openstreetmap-website/blob/master/test/controllers/geocoder_controller_test.rb
let expected;
beforeEach(() => {
expected = L.latLng(50.06773, 14.37742);
});
it('geocodes basic lat/lon pairs', () => {
geocode('50.06773 14.37742');
geocode('50.06773, 14.37742');
geocode('+50.06773 +14.37742');
geocode('+50.06773, +14.37742');
});
it('does not geocode no-lat-lng', () => {
const geocoder = new LatLng();
const callback = jest.fn();
geocoder.geocode('no-lat-lng', callback);
expect(callback).toHaveBeenCalledTimes(0);
});
it('passes unsupported queries to the next geocoder', () => {
const next = {
geocode: (_query, cb) => cb('XXX')
};
const geocoder = new LatLng({ next: next });
const callback = jest.fn();
geocoder.geocode('no-lat-lng', callback);
expect(callback).toHaveBeenCalledWith('XXX');
});
it('geocodes lat/lon pairs using N/E with degrees', () => {
geocode('N50.06773 E14.37742');
geocode('N50.06773, E14.37742');
geocode('50.06773N 14.37742E');
geocode('50.06773N, 14.37742E');
});
it('geocodes lat/lon pairs using N/W with degrees', () => {
expected = L.latLng(50.06773, -14.37742);
geocode('N50.06773 W14.37742');
geocode('N50.06773, W14.37742');
geocode('50.06773N 14.37742W');
geocode('50.06773N, 14.37742W');
});
it('geocodes lat/lon pairs using S/E with degrees', () => {
expected = L.latLng(-50.06773, 14.37742);
geocode('S50.06773 E14.37742');
geocode('S50.06773, E14.37742');
geocode('50.06773S 14.37742E');
geocode('50.06773S, 14.37742E');
});
it('geocodes lat/lon pairs using S/W with degrees', () => {
expected = L.latLng(-50.06773, -14.37742);
geocode('S50.06773 W14.37742');
geocode('S50.06773, W14.37742');
geocode('50.06773S 14.37742W');
geocode('50.06773S, 14.37742W');
});
it('geocodes lat/lon pairs using N/E with degrees/mins', () => {
expected = L.latLng(50.06773333333334, 14.377416666666667);
geocode('N 50° 04.064 E 014° 22.645');
geocode("N 50° 04.064' E 014° 22.645");
geocode("N 50° 04.064', E 014° 22.645'");
geocode('N50° 04.064 E14° 22.645');
geocode('N 50 04.064 E 014 22.645');
geocode('N50 4.064 E14 22.645');
geocode("50° 04.064' N, 014° 22.645' E");
});
it('geocodes lat/lon pairs using N/W with degrees/mins', () => {
expected = L.latLng(50.06773333333334, -14.377416666666667);
geocode('N 50° 04.064 W 014° 22.645');
geocode("N 50° 04.064' W 014° 22.645");
geocode("N 50° 04.064', W 014° 22.645'");
geocode('N50° 04.064 W14° 22.645');
geocode('N 50 04.064 W 014 22.645');
geocode('N50 4.064 W14 22.645');
geocode("50° 04.064' N, 014° 22.645' W");
});
it('geocodes lat/lon pairs using S/E with degrees/mins', () => {
expected = L.latLng(-50.06773333333334, 14.377416666666667);
geocode('S 50° 04.064 E 014° 22.645');
geocode("S 50° 04.064' E 014° 22.645");
geocode("S 50° 04.064', E 014° 22.645'");
geocode('S50° 04.064 E14° 22.645');
geocode('S 50 04.064 E 014 22.645');
geocode('S50 4.064 E14 22.645');
geocode("50° 04.064' S, 014° 22.645' E");
});
it('geocodes lat/lon pairs using S/W with degrees/mins', () => {
expected = L.latLng(-50.06773333333334, -14.377416666666667);
geocode('S 50° 04.064 W 014° 22.645');
geocode("S 50° 04.064' W 014° 22.645");
geocode("S 50° 04.064', W 014° 22.645'");
geocode('S50° 04.064 W14° 22.645');
geocode('S 50 04.064 W 014 22.645');
geocode('S50 4.064 W14 22.645');
geocode("50° 04.064' S, 014° 22.645' W");
});
it('geocodes lat/lon pairs using N/E with degrees/mins/secs', () => {
geocode('N 50° 4\' 03.828" E 14° 22\' 38.712"');
geocode('N 50° 4\' 03.828", E 14° 22\' 38.712"');
geocode('N 50° 4′ 03.828″, E 14° 22′ 38.712″');
geocode('N50 4 03.828 E14 22 38.712');
geocode('N50 4 03.828, E14 22 38.712');
geocode('50°4\'3.828"N 14°22\'38.712"E');
});
it('geocodes lat/lon pairs using N/W with degrees/mins/secs', () => {
expected = L.latLng(50.06773, -14.37742);
geocode('N 50° 4\' 03.828" W 14° 22\' 38.712"');
geocode('N 50° 4\' 03.828", W 14° 22\' 38.712"');
geocode('N 50° 4′ 03.828″, W 14° 22′ 38.712″');
geocode('N50 4 03.828 W14 22 38.712');
geocode('N50 4 03.828, W14 22 38.712');
geocode('50°4\'3.828"N 14°22\'38.712"W');
});
it('geocodes lat/lon pairs using S/E with degrees/mins/secs', () => {
expected = L.latLng(-50.06773, 14.37742);
geocode('S 50° 4\' 03.828" E 14° 22\' 38.712"');
geocode('S 50° 4\' 03.828", E 14° 22\' 38.712"');
geocode('S 50° 4′ 03.828″, E 14° 22′ 38.712″');
geocode('S50 4 03.828 E14 22 38.712');
geocode('S50 4 03.828, E14 22 38.712');
geocode('50°4\'3.828"S 14°22\'38.712"E');
});
it('geocodes lat/lon pairs using S/W with degrees/mins/secs', () => {
expected = L.latLng(-50.06773, -14.37742);
geocode('S 50° 4\' 03.828" W 14° 22\' 38.712"');
geocode('S 50° 4\' 03.828", W 14° 22\' 38.712"');
geocode('S 50° 4′ 03.828″, W 14° 22′ 38.712″');
geocode('S50 4 03.828 W14 22 38.712');
geocode('S50 4 03.828, W14 22 38.712');
geocode('50°4\'3.828"S 14°22\'38.712"W');
});
function geocode(query) {
const geocoder = new LatLng();
const callback = jest.fn();
geocoder.geocode(query, callback);
expect(callback).toBeCalledTimes(1);
const feature = callback.mock.calls[0][0][0];
expect(feature.name).toBe(query);
expect(feature.center.lat).toBeCloseTo(expected.lat);
expect(feature.center.lng).toBeCloseTo(expected.lng);
}
});
import { testXMLHttpRequest } from './mockXMLHttpRequest';
import { Mapbox } from '../src/geocoders/mapbox';
describe('L.Control.Geocoder.Mapbox', () => {
it('geocodes Milwaukee Ave', () => {
const geocoder = new Mapbox({ apiKey: '0123' });
const callback = jest.fn();
testXMLHttpRequest(
'https://api.mapbox.com/geocoding/v5/mapbox.places/Milwaukee%20Ave.json?access_token=0123',
{
type: 'FeatureCollection',
query: ['825', 's', 'milwaukee', 'ave', 'deerfield', 'il', '60015'],
features: [
{
id: 'address.4356035406756260',
type: 'Feature',
place_type: ['address'],
relevance: 1,
properties: {},
text: 'Milwaukee Ave',
place_name: '825 Milwaukee Ave, Deerfield, Illinois 60015, United States',
matching_text: 'South Milwaukee Avenue',
matching_place_name:
'825 South Milwaukee Avenue, Deerfield, Illinois 60015, United States',
center: [-87.921434, 42.166602],
geometry: {
type: 'Point',
coordinates: [-87.921434, 42.166602],
interpolated: true,
omitted: true
},
address: '825',
context: [
{
id: 'neighborhood.287187',
text: 'Lake Cook Road'
},
{
id: 'postcode.13903677306297990',
text: '60015'
},
{
id: 'place.5958304312090910',
wikidata: 'Q287895',
text: 'Deerfield'
},
{
id: 'region.3290978600358810',
short_code: 'US-IL',
wikidata: 'Q1204',
text: 'Illinois'
},
{
id: 'country.9053006287256050',
short_code: 'us',
wikidata: 'Q30',
text: 'United States'
}
]
}
],
attribution:
'NOTICE: © 2018 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained. POI(s) provided by Foursquare.'
},
() => geocoder.geocode('Milwaukee Ave', callback)
);
const feature = callback.mock.calls[0][0][0];
expect(feature.name).toBe('825 Milwaukee Ave, Deerfield, Illinois 60015, United States');
expect(feature.center).toStrictEqual({ lat: 42.166602, lng: -87.921434 });
expect(feature.bbox).toStrictEqual({
_northEast: { lat: 42.166602, lng: -87.921434 },
_southWest: { lat: 42.166602, lng: -87.921434 }
});
expect(callback.mock.calls).toMatchSnapshot();
});
});
export function mockXMLHttpRequest<T>(response: T): XMLHttpRequest {
const xhrMock: Partial<XMLHttpRequest> = {
open: jest.fn(),
send: jest.fn(),
setRequestHeader: jest.fn(),
readyState: 4,
status: 200,
response
};
jest.spyOn(window, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest);
return xhrMock as XMLHttpRequest;
}
export function testXMLHttpRequest<T>(url: string, response: T, trigger: () => void) {
const xhrMock = mockXMLHttpRequest(response);
trigger();
expect(xhrMock.open).toBeCalledWith('GET', url, true);
expect(xhrMock.setRequestHeader).toBeCalledWith('Accept', 'application/json');
(xhrMock.onreadystatechange as EventListener)(new Event(''));
}
import { testXMLHttpRequest } from './mockXMLHttpRequest';
import { Nominatim } from '../src/geocoders/nominatim';
describe('L.Control.Geocoder.Nominatim', () => {
const geocoder = new Nominatim();
it('geocodes Innsbruck', () => {
const callback = jest.fn();
testXMLHttpRequest(
'https://nominatim.openstreetmap.org/search?q=innsbruck&limit=5&format=json&addressdetails=1',
[
{
place_id: 199282228,
licence: 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
osm_type: 'relation',
osm_id: 8182617,
boundingbox: ['47.2583715', '47.2808566', '11.3811871', '11.418183'],
lat: '47.26951525',
lon: '11.3971372042211',
display_name: 'Innsbruck, Tyrol, Austria',
class: 'boundary',
type: 'administrative',
importance: 0.763909048330467,
icon:
'https://nominatim.openstreetmap.org/images/mapicons/poi_boundary_administrative.p.20.png',
address: {
city_district: 'Innsbruck',
city: 'Innsbruck',
county: 'Innsbruck',
state: 'Tyrol',
country: 'Austria',
country_code: 'at'
}
}
],
() => geocoder.geocode('innsbruck', callback)
);
const feature = callback.mock.calls[0][0][0];
expect(feature.name).toBe('Innsbruck, Tyrol, Austria');
expect(feature.html).toBe(
'<span class=""> Innsbruck </span><br/><span class="leaflet-control-geocoder-address-context">Tyrol Austria</span>'
);
expect(feature.properties.address).toStrictEqual({
city_district: 'Innsbruck',
city: 'Innsbruck',
county: 'Innsbruck',
state: 'Tyrol',
country: 'Austria',
country_code: 'at'
});
expect(callback.mock.calls).toMatchSnapshot();
});
it('reverse geocodes 47.3/11.3', () => {
const callback = jest.fn();
testXMLHttpRequest(
'https://nominatim.openstreetmap.org/reverse?lat=47.3&lon=11.3&zoom=9&addressdetails=1&format=json',
{
place_id: 197718025,
licence: 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright',
osm_type: 'relation',
osm_id: 78251,
lat: '47.2065094',
lon: '11.3836945900354',
display_name: 'Innsbruck-Land, Tyrol, Austria',
address: {
county: 'Innsbruck-Land',
state: 'Tyrol',
country: 'Austria',
country_code: 'at'
},
boundingbox: ['46.9624854', '47.4499229', '10.9896868', '11.7051742']
},
() => geocoder.reverse({ lat: 47.3, lng: 11.3 }, 131000, callback)
);
const feature = callback.mock.calls[0][0][0];
expect(feature.name).toBe('Innsbruck-Land, Tyrol, Austria');
expect(feature.html).toBe('<span class="">Tyrol Austria</span>');
expect(feature.properties.address).toStrictEqual({
county: 'Innsbruck-Land',
state: 'Tyrol',
country: 'Austria',
country_code: 'at'
});
expect(callback.mock.calls).toMatchSnapshot();
});
});
// import './vendor/openlocationcode';
import * as OpenLocationCode from './vendor/openlocationcode';
import { OpenLocationCode as Geocoder } from '../src/geocoders/open-location-code';
describe('L.Control.Geocoder.OpenLocationCode', () => {
const geocoder = new Geocoder({ OpenLocationCode: OpenLocationCode });
it('geocodes 9C3XGW4F+5V', () => {
const callback = jest.fn();
geocoder.geocode('9C3XGW4F+5V', callback);
const feature = callback.mock.calls[0][0][0];
expect(feature.name).toBe('9C3XGW4F+5V');
expect(feature.center.lat).toBeCloseTo(51.505437499999985);
expect(feature.center.lng).toBeCloseTo(-0.07531249999998124);
});
it('reverse geocodes 47.3/11.3', () => {
const callback = jest.fn();
geocoder.reverse({ lat: 47.3, lng: 11.3 }, 131000, callback);
const feature = callback.mock.calls[0][0][0];
expect(feature.name).toBe('8FVH8822+22');
});
});
import { testXMLHttpRequest } from './mockXMLHttpRequest';
import { Openrouteservice } from '../src/geocoders/pelias';
describe('L.Control.Geocoder.Openrouteservice', () => {
const geocoder = new Openrouteservice({ apiKey: '0123' });
it('geocodes Innsbruck', () => {
const callback = jest.fn();
testXMLHttpRequest(
'https://api.openrouteservice.org/geocode/search?api_key=0123&text=innsbruck',
{
geocoding: {
version: '0.2',
attribution: 'openrouteservice.org | OpenStreetMap contributors | Geocoding by Pelias',
query: {},
warnings: ["performance optimization: excluding 'address' layer"],
engine: { name: 'Pelias', author: 'Mapzen', version: '1.0' }
},
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: { type: 'Point', coordinates: [11.407851, 47.272308] },
properties: {
id: '101748061',
layer: 'locality',
source_id: '101748061',
name: 'Innsbruck',
confidence: 1,
match_type: 'exact',
accuracy: 'centroid',
country: 'Austria',
country_a: 'AUT',
region: 'Tirol',
region_a: 'TR',
county: 'Innsbruck',
county_a: 'IN',
localadmin: 'Innsbruck',
locality: 'Innsbruck',
continent: 'Europe',
label: 'Innsbruck, Austria'
},
bbox: [11.3218091258, 47.2470573997, 11.452584553, 47.29398]
}
],
bbox: [10.9896885523, 46.9624806033, 11.7051690163, 47.4499185397]
},
() => geocoder.geocode('innsbruck', callback)
);
const feature = callback.mock.calls[0][0][0];
expect(feature.name).toBe('Innsbruck, Austria');
expect(feature.center).toStrictEqual({ lat: 47.272308, lng: 11.407851 });
expect(feature.bbox).toStrictEqual({
_southWest: { lat: 47.2470573997, lng: 11.3218091258 },
_northEast: { lat: 47.29398, lng: 11.452584553 }
});
expect(callback.mock.calls).toMatchSnapshot();
});
});
import { testXMLHttpRequest } from './mockXMLHttpRequest';
import { Photon } from '../src/geocoders/photon';
import { GeocodingResult } from '../src/geocoders/api';
describe('L.Control.Geocoder.Photon', () => {
it('geocodes Innsbruck', () => {
const geocoder = new Photon();
const callback = jest.fn();
testXMLHttpRequest(
'https://photon.komoot.io/api/?q=Innsbruck',
{
features: [
{
geometry: { coordinates: [11.3927685, 47.2654296], type: 'Point' },
type: 'Feature',
properties: {
osm_id: 8182617,
osm_type: 'R',
extent: [11.3811871, 47.2808566, 11.4181209, 47.2583715],
country: 'Austria',
osm_key: 'place',
city: 'Innsbruck',
countrycode: 'AT',
osm_value: 'city',
name: 'Innsbruck',
state: 'Tyrol',
type: 'locality'
}
},
{
geometry: { coordinates: [11.3959095, 47.2690806], type: 'Point' },
type: 'Feature',
properties: {
osm_id: 7323902269,
country: 'Austria',
city: 'Innsbruck',
countrycode: 'AT',
postcode: '6020',
type: 'house',
osm_type: 'N',
osm_key: 'amenity',
housenumber: '1',
street: 'Universitätsstraße',
district: 'Innenstadt',
osm_value: 'music_school',
name: 'Mozarteum Innsbruck',
state: 'Tyrol'
}
}
],
type: 'FeatureCollection'
},
() => geocoder.geocode('Innsbruck', callback)
);
const feature: GeocodingResult = callback.mock.calls[0][0][0];
expect(feature.name).toBe('Innsbruck, Innsbruck, Tyrol, Austria');
expect(feature.center).toStrictEqual({ lat: 47.2654296, lng: 11.3927685 });
expect(feature.bbox).toStrictEqual({
_northEast: { lat: 47.2808566, lng: 11.4181209 },
_southWest: { lat: 47.2583715, lng: 11.3811871 }
});
expect(callback.mock.calls).toMatchSnapshot();
});
});
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Convert locations to and from short codes.
*
* Open Location Codes are short, 10-11 character codes that can be used instead
* of street addresses. The codes can be generated and decoded offline, and use
* a reduced character set that minimises the chance of codes including words.
*
* Codes are able to be shortened relative to a nearby location. This means that
* in many cases, only four to seven characters of the code are needed.
* To recover the original code, the same location is not required, as long as
* a nearby location is provided.
*
* Codes represent rectangular areas rather than points, and the longer the
* code, the smaller the area. A 10 character code represents a 13.5x13.5
* meter area (at the equator. An 11 character code represents approximately
* a 2.8x3.5 meter area.
*
* Two encoding algorithms are used. The first 10 characters are pairs of
* characters, one for latitude and one for latitude, using base 20. Each pair
* reduces the area of the code by a factor of 400. Only even code lengths are
* sensible, since an odd-numbered length would have sides in a ratio of 20:1.
*
* At position 11, the algorithm changes so that each character selects one
* position from a 4x5 grid. This allows single-character refinements.
*
* Examples:
*
* Encode a location, default accuracy:
* var code = OpenLocationCode.encode(47.365590, 8.524997);
*
* Encode a location using one stage of additional refinement:
* var code = OpenLocationCode.encode(47.365590, 8.524997, 11);
*
* Decode a full code:
* var coord = OpenLocationCode.decode(code);
* var msg = 'Center is ' + coord.latitudeCenter + ',' + coord.longitudeCenter;
*
* Attempt to trim the first characters from a code:
* var shortCode = OpenLocationCode.shorten('8FVC9G8F+6X', 47.5, 8.5);
*
* Recover the full code from a short code:
* var code = OpenLocationCode.recoverNearest('9G8F+6X', 47.4, 8.6);
* var code = OpenLocationCode.recoverNearest('8F+6X', 47.4, 8.6);
*/
(function(root, factory) {
/* global define, module */
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['b'], function(b) {
return (root.returnExportsGlobal = factory(b));
});
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like enviroments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals
root.OpenLocationCode = factory();
}
})(this, function() {
var OpenLocationCode = {};
/**
* Provides a normal precision code, approximately 14x14 meters.
* @const {number}
*/
OpenLocationCode.CODE_PRECISION_NORMAL = 10;
/**
* Provides an extra precision code, approximately 2x3 meters.
* @const {number}
*/
OpenLocationCode.CODE_PRECISION_EXTRA = 11;
// A separator used to break the code into two parts to aid memorability.
var SEPARATOR_ = '+';
// The number of characters to place before the separator.
var SEPARATOR_POSITION_ = 8;
// The character used to pad codes.
var PADDING_CHARACTER_ = '0';
// The character set used to encode the values.
var CODE_ALPHABET_ = '23456789CFGHJMPQRVWX';
// The base to use to convert numbers to/from.
var ENCODING_BASE_ = CODE_ALPHABET_.length;
// The maximum value for latitude in degrees.
var LATITUDE_MAX_ = 90;
// The maximum value for longitude in degrees.
var LONGITUDE_MAX_ = 180;
// Maxiumum code length using lat/lng pair encoding. The area of such a
// code is approximately 13x13 meters (at the equator), and should be suitable
// for identifying buildings. This excludes prefix and separator characters.
var PAIR_CODE_LENGTH_ = 10;
// The resolution values in degrees for each position in the lat/lng pair
// encoding. These give the place value of each position, and therefore the
// dimensions of the resulting area.
var PAIR_RESOLUTIONS_ = [20.0, 1.0, 0.05, 0.0025, 0.000125];
// Number of columns in the grid refinement method.
var GRID_COLUMNS_ = 4;
// Number of rows in the grid refinement method.
var GRID_ROWS_ = 5;
// Size of the initial grid in degrees.
var GRID_SIZE_DEGREES_ = 0.000125;
// Minimum length of a code that can be shortened.
var MIN_TRIMMABLE_CODE_LEN_ = 6;
/**
Returns the OLC alphabet.
*/
var getAlphabet = (OpenLocationCode.getAlphabet = function() {
return CODE_ALPHABET_;
});
/**
* Determines if a code is valid.
*
* To be valid, all characters must be from the Open Location Code character
* set with at most one separator. The separator can be in any even-numbered
* position up to the eighth digit.
*
* @param {string} code The string to check.
* @return {boolean} True if the string is a valid code.
*/
var isValid = (OpenLocationCode.isValid = function(code) {
if (!code || typeof code !== 'string') {
return false;
}
// The separator is required.
if (code.indexOf(SEPARATOR_) == -1) {
return false;
}
if (code.indexOf(SEPARATOR_) != code.lastIndexOf(SEPARATOR_)) {
return false;
}
// Is it the only character?
if (code.length == 1) {
return false;
}
// Is it in an illegal position?
if (code.indexOf(SEPARATOR_) > SEPARATOR_POSITION_ || code.indexOf(SEPARATOR_) % 2 == 1) {
return false;
}
// We can have an even number of padding characters before the separator,
// but then it must be the final character.
if (code.indexOf(PADDING_CHARACTER_) > -1) {
// Not allowed to start with them!
if (code.indexOf(PADDING_CHARACTER_) == 0) {
return false;
}
// There can only be one group and it must have even length.
var padMatch = code.match(new RegExp('(' + PADDING_CHARACTER_ + '+)', 'g'));
if (
padMatch.length > 1 ||
padMatch[0].length % 2 == 1 ||
padMatch[0].length > SEPARATOR_POSITION_ - 2
) {
return false;
}
// If the code is long enough to end with a separator, make sure it does.
if (code.charAt(code.length - 1) != SEPARATOR_) {
return false;
}
}
// If there are characters after the separator, make sure there isn't just
// one of them (not legal).
if (code.length - code.indexOf(SEPARATOR_) - 1 == 1) {
return false;
}
// Strip the separator and any padding characters.
code = code
.replace(new RegExp('\\' + SEPARATOR_ + '+'), '')
.replace(new RegExp(PADDING_CHARACTER_ + '+'), '');
// Check the code contains only valid characters.
for (var i = 0, len = code.length; i < len; i++) {
var character = code.charAt(i).toUpperCase();
if (character != SEPARATOR_ && CODE_ALPHABET_.indexOf(character) == -1) {
return false;
}
}
return true;
});
/**
* Determines if a code is a valid short code.
*
* @param {string} code The string to check.
* @return {boolean} True if the string can be produced by removing four or
* more characters from the start of a valid code.
*/
var isShort = (OpenLocationCode.isShort = function(code) {
// Check it's valid.
if (!isValid(code)) {
return false;
}
// If there are less characters than expected before the SEPARATOR.
if (code.indexOf(SEPARATOR_) >= 0 && code.indexOf(SEPARATOR_) < SEPARATOR_POSITION_) {
return true;
}
return false;
});
/**
* Determines if a code is a valid full Open Location Code.
*
* @param {string} code The string to check.
* @return {boolean} True if the code represents a valid latitude and
* longitude combination.
*/
var isFull = (OpenLocationCode.isFull = function(code) {
if (!isValid(code)) {
return false;
}
// If it's short, it's not full.
if (isShort(code)) {
return false;
}
// Work out what the first latitude character indicates for latitude.
var firstLatValue = CODE_ALPHABET_.indexOf(code.charAt(0).toUpperCase()) * ENCODING_BASE_;
if (firstLatValue >= LATITUDE_MAX_ * 2) {
// The code would decode to a latitude of >= 90 degrees.
return false;
}
if (code.length > 1) {
// Work out what the first longitude character indicates for longitude.
var firstLngValue = CODE_ALPHABET_.indexOf(code.charAt(1).toUpperCase()) * ENCODING_BASE_;
if (firstLngValue >= LONGITUDE_MAX_ * 2) {
// The code would decode to a longitude of >= 180 degrees.
return false;
}
}
return true;
});
/**
* Encode a location into an Open Location Code.
*
* @param {number} latitude The latitude in signed decimal degrees. It will
* be clipped to the range -90 to 90.
* @param {number} longitude The longitude in signed decimal degrees. Will be
* normalised to the range -180 to 180.
* @param {?number} codeLength The length of the code to generate. If
* omitted, the value OpenLocationCode.CODE_PRECISION_NORMAL will be used.
* For a more precise result, OpenLocationCode.CODE_PRECISION_EXTRA is
* recommended.
* @return {string} The code.
* @throws {Exception} if any of the input values are not numbers.
*/
var encode = (OpenLocationCode.encode = function(latitude, longitude, codeLength) {
latitude = Number(latitude);
longitude = Number(longitude);
if (typeof codeLength == 'undefined') {
codeLength = OpenLocationCode.CODE_PRECISION_NORMAL;
} else {
codeLength = Number(codeLength);
}
if (isNaN(latitude) || isNaN(longitude) || isNaN(codeLength)) {
throw 'ValueError: Parameters are not numbers';
}
if (codeLength < 2 || (codeLength < SEPARATOR_POSITION_ && codeLength % 2 == 1)) {
throw 'IllegalArgumentException: Invalid Open Location Code length';
}
// Ensure that latitude and longitude are valid.
latitude = clipLatitude(latitude);
longitude = normalizeLongitude(longitude);
// Latitude 90 needs to be adjusted to be just less, so the returned code
// can also be decoded.
if (latitude == 90) {
latitude = latitude - computeLatitudePrecision(codeLength);
}
var code = encodePairs(latitude, longitude, Math.min(codeLength, PAIR_CODE_LENGTH_));
// If the requested length indicates we want grid refined codes.
if (codeLength > PAIR_CODE_LENGTH_) {
code += encodeGrid(latitude, longitude, codeLength - PAIR_CODE_LENGTH_);
}
return code;
});
/**
* Decodes an Open Location Code into its location coordinates.
*
* Returns a CodeArea object that includes the coordinates of the bounding
* box - the lower left, center and upper right.
*
* @param {string} code The code to decode.
* @return {OpenLocationCode.CodeArea} An object with the coordinates of the
* area of the code.
* @throws {Exception} If the code is not valid.
*/
var decode = (OpenLocationCode.decode = function(code) {
if (!isFull(code)) {
throw 'IllegalArgumentException: ' +
'Passed Open Location Code is not a valid full code: ' +
code;
}
// Strip out separator character (we've already established the code is
// valid so the maximum is one), padding characters and convert to upper
// case.
code = code.replace(SEPARATOR_, '');
code = code.replace(new RegExp(PADDING_CHARACTER_ + '+'), '');
code = code.toUpperCase();
// Decode the lat/lng pair component.
var codeArea = decodePairs(code.substring(0, PAIR_CODE_LENGTH_));
// If there is a grid refinement component, decode that.
if (code.length <= PAIR_CODE_LENGTH_) {
return codeArea;
}
var gridArea = decodeGrid(code.substring(PAIR_CODE_LENGTH_));
return CodeArea(
codeArea.latitudeLo + gridArea.latitudeLo,
codeArea.longitudeLo + gridArea.longitudeLo,
codeArea.latitudeLo + gridArea.latitudeHi,
codeArea.longitudeLo + gridArea.longitudeHi,
codeArea.codeLength + gridArea.codeLength
);
});
/**
* Recover the nearest matching code to a specified location.
*
* Given a valid short Open Location Code this recovers the nearest matching
* full code to the specified location.
*
* @param {string} shortCode A valid short code.
* @param {number} referenceLatitude The latitude to use for the reference
* location.
* @param {number} referenceLongitude The longitude to use for the reference
* location.
* @return {string} The nearest matching full code to the reference location.
* @throws {Exception} if the short code is not valid, or the reference
* position values are not numbers.
*/
var recoverNearest = (OpenLocationCode.recoverNearest = function(
shortCode,
referenceLatitude,
referenceLongitude
) {
if (!isShort(shortCode)) {
if (isFull(shortCode)) {
return shortCode;
} else {
throw 'ValueError: Passed short code is not valid: ' + shortCode;
}
}
referenceLatitude = Number(referenceLatitude);
referenceLongitude = Number(referenceLongitude);
if (isNaN(referenceLatitude) || isNaN(referenceLongitude)) {
throw 'ValueError: Reference position are not numbers';
}
// Ensure that latitude and longitude are valid.
referenceLatitude = clipLatitude(referenceLatitude);
referenceLongitude = normalizeLongitude(referenceLongitude);
// Clean up the passed code.
shortCode = shortCode.toUpperCase();
// Compute the number of digits we need to recover.
var paddingLength = SEPARATOR_POSITION_ - shortCode.indexOf(SEPARATOR_);
// The resolution (height and width) of the padded area in degrees.
var resolution = Math.pow(20, 2 - paddingLength / 2);
// Distance from the center to an edge (in degrees).
var areaToEdge = resolution / 2.0;
// Use the reference location to pad the supplied short code and decode it.
var codeArea = decode(
encode(referenceLatitude, referenceLongitude).substr(0, paddingLength) + shortCode
);
// How many degrees latitude is the code from the reference? If it is more
// than half the resolution, we need to move it east or west.
var degreesDifference = codeArea.latitudeCenter - referenceLatitude;
if (degreesDifference > areaToEdge) {
// If the center of the short code is more than half a cell east,
// then the best match will be one position west.
codeArea.latitudeCenter -= resolution;
} else if (degreesDifference < -areaToEdge) {
// If the center of the short code is more than half a cell west,
// then the best match will be one position east.
codeArea.latitudeCenter += resolution;
}
// How many degrees longitude is the code from the reference?
degreesDifference = codeArea.longitudeCenter - referenceLongitude;
if (degreesDifference > areaToEdge) {
codeArea.longitudeCenter -= resolution;
} else if (degreesDifference < -areaToEdge) {
codeArea.longitudeCenter += resolution;
}
return encode(codeArea.latitudeCenter, codeArea.longitudeCenter, codeArea.codeLength);
});
/**
* Remove characters from the start of an OLC code.
*
* This uses a reference location to determine how many initial characters
* can be removed from the OLC code. The number of characters that can be
* removed depends on the distance between the code center and the reference
* location.
*
* @param {string} code The full code to shorten.
* @param {number} latitude The latitude to use for the reference location.
* @param {number} longitude The longitude to use for the reference location.
* @return {string} The code, shortened as much as possible that it is still
* the closest matching code to the reference location.
* @throws {Exception} if the passed code is not a valid full code or the
* reference location values are not numbers.
*/
var shorten = (OpenLocationCode.shorten = function(code, latitude, longitude) {
if (!isFull(code)) {
throw 'ValueError: Passed code is not valid and full: ' + code;
}
if (code.indexOf(PADDING_CHARACTER_) != -1) {
throw 'ValueError: Cannot shorten padded codes: ' + code;
}
var code = code.toUpperCase();
var codeArea = decode(code);
if (codeArea.codeLength < MIN_TRIMMABLE_CODE_LEN_) {
throw 'ValueError: Code length must be at least ' + MIN_TRIMMABLE_CODE_LEN_;
}
// Ensure that latitude and longitude are valid.
latitude = Number(latitude);
longitude = Number(longitude);
if (isNaN(latitude) || isNaN(longitude)) {
throw 'ValueError: Reference position are not numbers';
}
latitude = clipLatitude(latitude);
longitude = normalizeLongitude(longitude);
// How close are the latitude and longitude to the code center.
var range = Math.max(
Math.abs(codeArea.latitudeCenter - latitude),
Math.abs(codeArea.longitudeCenter - longitude)
);
for (var i = PAIR_RESOLUTIONS_.length - 2; i >= 1; i--) {
// Check if we're close enough to shorten. The range must be less than 1/2
// the resolution to shorten at all, and we want to allow some safety, so
// use 0.3 instead of 0.5 as a multiplier.
if (range < PAIR_RESOLUTIONS_[i] * 0.3) {
// Trim it.
return code.substring((i + 1) * 2);
}
}
return code;
});
/**
* Clip a latitude into the range -90 to 90.
*
* @param {number} latitude
* @return {number} The latitude value clipped to be in the range.
*/
var clipLatitude = function(latitude) {
return Math.min(90, Math.max(-90, latitude));
};
/**
* Compute the latitude precision value for a given code length.
* Lengths <= 10 have the same precision for latitude and longitude, but
* lengths > 10 have different precisions due to the grid method having
* fewer columns than rows.
* @param {number} codeLength
* @return {number} The latitude precision in degrees.
*/
var computeLatitudePrecision = function(codeLength) {
if (codeLength <= 10) {
return Math.pow(20, Math.floor(codeLength / -2 + 2));
}
return Math.pow(20, -3) / Math.pow(GRID_ROWS_, codeLength - 10);
};
/**
* Normalize a longitude into the range -180 to 180, not including 180.
*
* @param {number} longitude
* @return {number} Normalized into the range -180 to 180.
*/
var normalizeLongitude = function(longitude) {
while (longitude < -180) {
longitude = longitude + 360;
}
while (longitude >= 180) {
longitude = longitude - 360;
}
return longitude;
};
/**
* Encode a location into a sequence of OLC lat/lng pairs.
*
* This uses pairs of characters (longitude and latitude in that order) to
* represent each step in a 20x20 grid. Each code, therefore, has 1/400th
* the area of the previous code.
*
* This algorithm is used up to 10 digits.
*
* @param {number} latitude The location to encode.
* @param {number} longitude The location to encode.
* @param {number} codeLength Requested code length.
* @return {string} The up to 10-digit OLC code for the location.
*/
var encodePairs = function(latitude, longitude, codeLength) {
var code = '';
// Adjust latitude and longitude so they fall into positive ranges.
var adjustedLatitude = latitude + LATITUDE_MAX_;
var adjustedLongitude = longitude + LONGITUDE_MAX_;
// Count digits - can't use string length because it may include a separator
// character.
var digitCount = 0;
while (digitCount < codeLength) {
// Provides the value of digits in this place in decimal degrees.
var placeValue = PAIR_RESOLUTIONS_[Math.floor(digitCount / 2)];
// Do the latitude - gets the digit for this place and subtracts that for
// the next digit.
var digitValue = Math.floor(adjustedLatitude / placeValue);
adjustedLatitude -= digitValue * placeValue;
code += CODE_ALPHABET_.charAt(digitValue);
digitCount += 1;
// And do the longitude - gets the digit for this place and subtracts that
// for the next digit.
digitValue = Math.floor(adjustedLongitude / placeValue);
adjustedLongitude -= digitValue * placeValue;
code += CODE_ALPHABET_.charAt(digitValue);
digitCount += 1;
// Should we add a separator here?
if (digitCount == SEPARATOR_POSITION_ && digitCount < codeLength) {
code += SEPARATOR_;
}
}
if (code.length < SEPARATOR_POSITION_) {
code = code + Array(SEPARATOR_POSITION_ - code.length + 1).join(PADDING_CHARACTER_);
}
if (code.length == SEPARATOR_POSITION_) {
code = code + SEPARATOR_;
}
return code;
};
/**
* Encode a location using the grid refinement method into an OLC string.
*
* The grid refinement method divides the area into a grid of 4x5, and uses a
* single character to refine the area. This allows default accuracy OLC codes
* to be refined with just a single character.
*
* This algorithm is used for codes longer than 10 digits.
*
* @param {number} latitude The location to encode.
* @param {number} longitude The location to encode.
* @param {number} codeLength Requested code length.
* @return {string} The OLC code digits from the 11th digit on.
*/
var encodeGrid = function(latitude, longitude, codeLength) {
var code = '';
var latPlaceValue = GRID_SIZE_DEGREES_;
var lngPlaceValue = GRID_SIZE_DEGREES_;
// Adjust latitude and longitude so they fall into positive ranges and
// get the offset for the required places.
var adjustedLatitude = (latitude + LATITUDE_MAX_) % latPlaceValue;
var adjustedLongitude = (longitude + LONGITUDE_MAX_) % lngPlaceValue;
for (var i = 0; i < codeLength; i++) {
// Work out the row and column.
var row = Math.floor(adjustedLatitude / (latPlaceValue / GRID_ROWS_));
var col = Math.floor(adjustedLongitude / (lngPlaceValue / GRID_COLUMNS_));
latPlaceValue /= GRID_ROWS_;
lngPlaceValue /= GRID_COLUMNS_;
adjustedLatitude -= row * latPlaceValue;
adjustedLongitude -= col * lngPlaceValue;
code += CODE_ALPHABET_.charAt(row * GRID_COLUMNS_ + col);
}
return code;
};
/**
* Decode an OLC code made up of lat/lng pairs.
*
* This decodes an OLC code made up of alternating latitude and longitude
* characters, encoded using base 20.
*
* @param {string} code The code to decode, assumed to be a valid full code,
* but with the separator removed.
* @return {OpenLocationCode.CodeArea} The code area object.
*/
var decodePairs = function(code) {
// Get the latitude and longitude values. These will need correcting from
// positive ranges.
var latitude = decodePairsSequence(code, 0);
var longitude = decodePairsSequence(code, 1);
// Correct the values and set them into the CodeArea object.
return new CodeArea(
latitude[0] - LATITUDE_MAX_,
longitude[0] - LONGITUDE_MAX_,
latitude[1] - LATITUDE_MAX_,
longitude[1] - LONGITUDE_MAX_,
code.length
);
};
/**
* Decode either a latitude or longitude sequence.
*
* This decodes the latitude or longitude sequence of a lat/lng pair encoding.
* Starting at the character at position offset, every second character is
* decoded and the value returned.
*
* @param {string} code A valid full OLC code, with the separator removed.
* @param {string} offset The character to start from.
* @return {[number]} An array of two numbers, representing the lower and
* upper range in decimal degrees. These are in positive ranges and will
* need to be corrected appropriately.
*/
var decodePairsSequence = function(code, offset) {
var i = 0;
var value = 0;
while (i * 2 + offset < code.length) {
value += CODE_ALPHABET_.indexOf(code.charAt(i * 2 + offset)) * PAIR_RESOLUTIONS_[i];
i += 1;
}
return [value, value + PAIR_RESOLUTIONS_[i - 1]];
};
/**
* Decode the grid refinement portion of an OLC code.
*
* @param {string} code The grid refinement section of a code.
* @return {OpenLocationCode.CodeArea} The area of the code.
*/
var decodeGrid = function(code) {
var latitudeLo = 0.0;
var longitudeLo = 0.0;
var latPlaceValue = GRID_SIZE_DEGREES_;
var lngPlaceValue = GRID_SIZE_DEGREES_;
var i = 0;
while (i < code.length) {
var codeIndex = CODE_ALPHABET_.indexOf(code.charAt(i));
var row = Math.floor(codeIndex / GRID_COLUMNS_);
var col = codeIndex % GRID_COLUMNS_;
latPlaceValue /= GRID_ROWS_;
lngPlaceValue /= GRID_COLUMNS_;
latitudeLo += row * latPlaceValue;
longitudeLo += col * lngPlaceValue;
i += 1;
}
return CodeArea(
latitudeLo,
longitudeLo,
latitudeLo + latPlaceValue,
longitudeLo + lngPlaceValue,
code.length
);
};
/**
* Coordinates of a decoded Open Location Code.
*
* The coordinates include the latitude and longitude of the lower left and
* upper right corners and the center of the bounding box for the area the
* code represents.
*
* @constructor
*/
var CodeArea = (OpenLocationCode.CodeArea = function(
latitudeLo,
longitudeLo,
latitudeHi,
longitudeHi,
codeLength
) {
return new OpenLocationCode.CodeArea.fn.init(
latitudeLo,
longitudeLo,
latitudeHi,
longitudeHi,
codeLength
);
});
CodeArea.fn = CodeArea.prototype = {
init: function(latitudeLo, longitudeLo, latitudeHi, longitudeHi, codeLength) {
/**
* The latitude of the SW corner.
* @type {number}
*/
this.latitudeLo = latitudeLo;
/**
* The longitude of the SW corner in degrees.
* @type {number}
*/
this.longitudeLo = longitudeLo;
/**
* The latitude of the NE corner in degrees.
* @type {number}
*/
this.latitudeHi = latitudeHi;
/**
* The longitude of the NE corner in degrees.
* @type {number}
*/
this.longitudeHi = longitudeHi;
/**
* The number of digits in the code.
* @type {number}
*/
this.codeLength = codeLength;
/**
* The latitude of the center in degrees.
* @type {number}
*/
this.latitudeCenter = Math.min(latitudeLo + (latitudeHi - latitudeLo) / 2, LATITUDE_MAX_);
/**
* The longitude of the center in degrees.
* @type {number}
*/
this.longitudeCenter = Math.min(
longitudeLo + (longitudeHi - longitudeLo) / 2,
LONGITUDE_MAX_
);
}
};
CodeArea.fn.init.prototype = CodeArea.fn;
return OpenLocationCode;
});
import * as L from 'leaflet';
import { Nominatim } from './geocoders/index';
import { IGeocoder, GeocodingResult } from './geocoders/api';
export interface GeocoderControlOptions extends L.ControlOptions {
/**
* Collapse control unless hovered/clicked
*/
collapsed: boolean;
/**
* How to expand a collapsed control: `touch` or `click` or `hover`
*/
expand: 'touch' | 'click' | 'hover';
/**
* Placeholder text for text input
*/
placeholder: string;
/**
* Message when no result found / geocoding error occurs
*/
errorMessage: string;
/**
* Accessibility label for the search icon used by screen readers
*/
iconLabel: string;
/**
* Object to perform the actual geocoding queries
*/
geocoder?: IGeocoder;
/**
* Immediately show the unique result without prompting for alternatives
*/
showUniqueResult: boolean;
/**
* Show icons for geocoding results (if available); supported by Nominatim
*/
showResultIcons: boolean;
/**
* Minimum number characters before suggest functionality is used (if available from geocoder)
*/
suggestMinLength: number;
/**
* Number of milliseconds after typing stopped before suggest functionality is used (if available from geocoder)
*/
suggestTimeout: number;
/**
* Initial query string for text input
*/
query: string;
/**
* Minimum number of characters in search text before performing a query
*/
queryMinLength: number;
/**
* Whether to mark a geocoding result on the map by default
*/
defaultMarkGeocode: boolean;
}
/**
* Event is fired when selecting a geocode result.
* By default, the control will center the map on it and place a marker at its location.
* To remove the control's default handler for marking a result, set {@link GeocoderControlOptions.defaultMarkGeocode} to `false`.
*/
export type MarkGeocodeEvent = { geocode: GeocodingResult };
export type MarkGeocodeEventHandlerFn = (event: MarkGeocodeEvent) => void;
/**
* Event is fired before invoking {@link IGeocoder.geocode} (or {@link IGeocoder.suggest}).
* The event data contains the query string as `input`.
*/
export type StartGeocodeEvent = { input: string };
export type StartGeocodeEventHandlerFn = (event: StartGeocodeEvent) => void;
/**
* Event is fired before after receiving results from {@link IGeocoder.geocode} (or {@link IGeocoder.suggest}).
* The event data contains the query string as `input` and the geocoding `results`.
*/
export type FinishGeocodeEvent = { input: string; results: GeocodingResult[] };
export type FinishGeocodeEventHandlerFn = (event: FinishGeocodeEvent) => void;
declare module 'leaflet' {
interface Evented {
on(type: 'markgeocode', fn: MarkGeocodeEventHandlerFn, context?: any): this;
on(type: 'startgeocode', fn: StartGeocodeEventHandlerFn, context?: any): this;
on(type: 'startsuggest', fn: StartGeocodeEventHandlerFn, context?: any): this;
on(type: 'finishsuggest', fn: FinishGeocodeEventHandlerFn, context?: any): this;
on(type: 'finishgeocode', fn: FinishGeocodeEventHandlerFn, context?: any): this;
}
}
/**
* Leaflet mixins https://leafletjs.com/reference-1.7.1.html#class-includes
* for TypeScript https://www.typescriptlang.org/docs/handbook/mixins.html
* @internal
*/
class EventedControl {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(...args: any[]) {
// empty
}
}
/**
* @internal
*/
interface EventedControl extends L.Control, L.Evented {}
L.Util.extend(EventedControl.prototype, L.Control.prototype);
L.Util.extend(EventedControl.prototype, L.Evented.prototype);
/**
* This is the geocoder control. It works like any other [Leaflet control](https://leafletjs.com/reference.html#control), and is added to the map.
*/
export class GeocoderControl extends EventedControl {
options: GeocoderControlOptions = {
showUniqueResult: true,
showResultIcons: false,
collapsed: true,
expand: 'touch',
position: 'topright',
placeholder: 'Search...',
errorMessage: 'Nothing found.',
iconLabel: 'Initiate a new search',
query: '',
queryMinLength: 1,
suggestMinLength: 3,
suggestTimeout: 250,
defaultMarkGeocode: true
};
private _alts: HTMLUListElement;
private _container: HTMLDivElement;
private _errorElement: HTMLDivElement;
private _form: HTMLDivElement;
private _geocodeMarker: L.Marker;
private _input: HTMLInputElement;
private _lastGeocode: string;
private _map: L.Map;
private _preventBlurCollapse: boolean;
private _requestCount = 0;
private _results: any;
private _selection: any;
private _suggestTimeout: any;
/**
* Instantiates a geocoder control (to be invoked using `new`)
* @param options the options
*/
constructor(options?: Partial<GeocoderControlOptions>) {
super(options);
L.Util.setOptions(this, options);
if (!this.options.geocoder) {
this.options.geocoder = new Nominatim();
}
}
addThrobberClass() {
L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-throbber');
}
removeThrobberClass() {
L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-throbber');
}
/**
* Returns the container DOM element for the control and add listeners on relevant map events.
* @param map the map instance
* @see https://leafletjs.com/reference.html#control-onadd
*/
onAdd(map: L.Map) {
const className = 'leaflet-control-geocoder';
const container = L.DomUtil.create('div', className + ' leaflet-bar') as HTMLDivElement;
const icon = L.DomUtil.create('button', className + '-icon', container) as HTMLButtonElement;
const form = (this._form = L.DomUtil.create(
'div',
className + '-form',
container
) as HTMLDivElement);
this._map = map;
this._container = container;
icon.innerHTML = '&nbsp;';
icon.type = 'button';
icon.setAttribute('aria-label', this.options.iconLabel);
const input = (this._input = L.DomUtil.create('input', '', form) as HTMLInputElement);
input.type = 'text';
input.value = this.options.query;
input.placeholder = this.options.placeholder;
L.DomEvent.disableClickPropagation(input);
this._errorElement = L.DomUtil.create(
'div',
className + '-form-no-error',
container
) as HTMLDivElement;
this._errorElement.innerHTML = this.options.errorMessage;
this._alts = L.DomUtil.create(
'ul',
className + '-alternatives leaflet-control-geocoder-alternatives-minimized',
container
) as HTMLUListElement;
L.DomEvent.disableClickPropagation(this._alts);
L.DomEvent.addListener(input, 'keydown', this._keydown, this);
if (this.options.geocoder.suggest) {
L.DomEvent.addListener(input, 'input', this._change, this);
}
L.DomEvent.addListener(input, 'blur', () => {
if (this.options.collapsed && !this._preventBlurCollapse) {
this._collapse();
}
this._preventBlurCollapse = false;
});
if (this.options.collapsed) {
if (this.options.expand === 'click') {
L.DomEvent.addListener(container, 'click', (e: Event) => {
if ((e as MouseEvent).button === 0 && (e as MouseEvent).detail !== 2) {
this._toggle();
}
});
} else if (this.options.expand === 'touch') {
L.DomEvent.addListener(
container,
L.Browser.touch ? 'touchstart mousedown' : 'mousedown',
(e: Event) => {
this._toggle();
e.preventDefault(); // mobile: clicking focuses the icon, so UI expands and immediately collapses
e.stopPropagation();
},
this
);
} else {
L.DomEvent.addListener(container, 'mouseover', this._expand, this);
L.DomEvent.addListener(container, 'mouseout', this._collapse, this);
this._map.on('movestart', this._collapse, this);
}
} else {
this._expand();
if (L.Browser.touch) {
L.DomEvent.addListener(container, 'touchstart', () => this._geocode());
} else {
L.DomEvent.addListener(container, 'click', () => this._geocode());
}
}
if (this.options.defaultMarkGeocode) {
this.on('markgeocode', this.markGeocode, this);
}
this.on('startgeocode', this.addThrobberClass, this);
this.on('finishgeocode', this.removeThrobberClass, this);
this.on('startsuggest', this.addThrobberClass, this);
this.on('finishsuggest', this.removeThrobberClass, this);
L.DomEvent.disableClickPropagation(container);
return container;
}
/**
* Sets the query string on the text input
* @param string the query string
*/
setQuery(string: string): this {
this._input.value = string;
return this;
}
private _geocodeResult(results: GeocodingResult[], suggest: boolean) {
if (!suggest && this.options.showUniqueResult && results.length === 1) {
this._geocodeResultSelected(results[0]);
} else if (results.length > 0) {
this._alts.innerHTML = '';
this._results = results;
L.DomUtil.removeClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');
L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-options-open');
for (let i = 0; i < results.length; i++) {
this._alts.appendChild(this._createAlt(results[i], i));
}
} else {
L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-options-error');
L.DomUtil.addClass(this._errorElement, 'leaflet-control-geocoder-error');
}
}
/**
* Marks a geocoding result on the map
* @param result the geocoding result
*/
markGeocode(event: MarkGeocodeEvent) {
const result = event.geocode;
this._map.fitBounds(result.bbox);
if (this._geocodeMarker) {
this._map.removeLayer(this._geocodeMarker);
}
this._geocodeMarker = new L.Marker(result.center)
.bindPopup(result.html || result.name)
.addTo(this._map)
.openPopup();
return this;
}
private _geocode(suggest?: boolean) {
const value = this._input.value;
if (!suggest && value.length < this.options.queryMinLength) {
return;
}
const requestCount = ++this._requestCount;
const cb = (results: GeocodingResult[]) => {
if (requestCount === this._requestCount) {
const event: FinishGeocodeEvent = { input: value, results };
this.fire(suggest ? 'finishsuggest' : 'finishgeocode', event);
this._geocodeResult(results, suggest);
}
};
this._lastGeocode = value;
if (!suggest) {
this._clearResults();
}
const event: StartGeocodeEvent = { input: value };
this.fire(suggest ? 'startsuggest' : 'startgeocode', event);
if (suggest) {
this.options.geocoder.suggest(value, cb);
} else {
this.options.geocoder.geocode(value, cb);
}
}
private _geocodeResultSelected(geocode: GeocodingResult) {
const event: MarkGeocodeEvent = { geocode };
this.fire('markgeocode', event);
}
private _toggle() {
if (L.DomUtil.hasClass(this._container, 'leaflet-control-geocoder-expanded')) {
this._collapse();
} else {
this._expand();
}
}
private _expand() {
L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-expanded');
this._input.select();
this.fire('expand');
}
private _collapse() {
L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-expanded');
L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');
L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error');
L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-options-open');
L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-options-error');
this._input.blur(); // mobile: keyboard shouldn't stay expanded
this.fire('collapse');
}
private _clearResults() {
L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');
this._selection = null;
L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error');
L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-options-open');
L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-options-error');
}
private _createAlt(result: GeocodingResult, index: number) {
const li = L.DomUtil.create('li', ''),
a = L.DomUtil.create('a', '', li),
icon =
this.options.showResultIcons && result.icon
? (L.DomUtil.create('img', '', a) as HTMLImageElement)
: null,
text = result.html ? undefined : document.createTextNode(result.name),
mouseDownHandler = (e: Event) => {
// In some browsers, a click will fire on the map if the control is
// collapsed directly after mousedown. To work around this, we
// wait until the click is completed, and _then_ collapse the
// control. Messy, but this is the workaround I could come up with
// for #142.
this._preventBlurCollapse = true;
L.DomEvent.stop(e);
this._geocodeResultSelected(result);
L.DomEvent.on(li, 'click touchend', () => {
if (this.options.collapsed) {
this._collapse();
} else {
this._clearResults();
}
});
};
if (icon) {
icon.src = result.icon;
}
li.setAttribute('data-result-index', String(index));
if (result.html) {
a.innerHTML = a.innerHTML + result.html;
} else if (text) {
a.appendChild(text);
}
// Use mousedown and not click, since click will fire _after_ blur,
// causing the control to have collapsed and removed the items
// before the click can fire.
L.DomEvent.addListener(li, 'mousedown touchstart', mouseDownHandler, this);
return li;
}
private _keydown(e: KeyboardEvent) {
const select = (dir: number) => {
if (this._selection) {
L.DomUtil.removeClass(this._selection, 'leaflet-control-geocoder-selected');
this._selection = this._selection[dir > 0 ? 'nextSibling' : 'previousSibling'];
}
if (!this._selection) {
this._selection = this._alts[dir > 0 ? 'firstChild' : 'lastChild'];
}
if (this._selection) {
L.DomUtil.addClass(this._selection, 'leaflet-control-geocoder-selected');
}
};
switch (e.keyCode) {
// Escape
case 27:
if (this.options.collapsed) {
this._collapse();
} else {
this._clearResults();
}
break;
// Up
case 38:
select(-1);
break;
// Up
case 40:
select(1);
break;
// Enter
case 13:
if (this._selection) {
const index = parseInt(this._selection.getAttribute('data-result-index'), 10);
this._geocodeResultSelected(this._results[index]);
this._clearResults();
} else {
this._geocode();
}
break;
default:
return;
}
L.DomEvent.preventDefault(e);
}
private _change() {
const v = this._input.value;
if (v !== this._lastGeocode) {
clearTimeout(this._suggestTimeout);
if (v.length >= this.options.suggestMinLength) {
this._suggestTimeout = setTimeout(() => this._geocode(true), this.options.suggestTimeout);
} else {
this._clearResults();
}
}
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link GeocoderControl}
* @param options the options
*/
export function geocoder(options?: Partial<GeocoderControlOptions>) {
return new GeocoderControl(options);
}
import * as L from 'leaflet';
/**
* An object that represents a result from a geocoding query
*/
export interface GeocodingResult {
/**
* Name of found location
*/
name: string;
/**
* The bounds of the location
*/
bbox: L.LatLngBounds;
/**
* The center coordinate of the location
*/
center: L.LatLng;
/**
* URL for icon representing result; optional
*/
icon?: string;
/**
* HTML formatted representation of the name
*/
html?: string;
/**
* Additional properties returned by the geocoder
*/
properties?: any;
}
/**
* A callback function used in {@link IGeocoder.geocode} and {@link IGeocoder.suggest} and {@link IGeocoder.reverse}
*/
export type GeocodingCallback = (result: GeocodingResult[]) => void;
/**
* An interface implemented to respond to geocoding queries
*/
export interface IGeocoder {
/**
* Performs a geocoding query and returns the results to the callback in the provided context
* @param query the query
* @param cb the callback function
* @param context the `this` context in the callback
*/
geocode(query: string, cb: GeocodingCallback, context?: any): void;
/**
* Performs a geocoding query suggestion (this happens while typing) and returns the results to the callback in the provided context
* @param query the query
* @param cb the callback function
* @param context the `this` context in the callback
*/
suggest?(query: string, cb: GeocodingCallback, context?: any): void;
/**
* Performs a reverse geocoding query and returns the results to the callback in the provided context
* @param location the coordinate to reverse geocode
* @param scale the map scale possibly used for reverse geocoding
* @param cb the callback function
* @param context the `this` context in the callback
*/
reverse?(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void;
}
export interface GeocoderOptions {
/**
* URL of the service
*/
serviceUrl: string;
/**
* Additional URL parameters (strings) that will be added to geocoding requests
*/
geocodingQueryParams?: Record<string, unknown>;
/**
* Additional URL parameters (strings) that will be added to reverse geocoding requests
*/
reverseQueryParams?: Record<string, unknown>;
/**
* API key to use this service
*/
apiKey?: string;
}
/**
* @internal
*/
export function geocodingParams(
options: GeocoderOptions,
params: Record<string, unknown>
): Record<string, unknown> {
return L.Util.extend(params, options.geocodingQueryParams);
}
/**
* @internal
*/
export function reverseParams(
options: GeocoderOptions,
params: Record<string, unknown>
): Record<string, unknown> {
return L.Util.extend(params, options.reverseQueryParams);
}
import * as L from 'leaflet';
import { getJSON } from '../util';
import {
IGeocoder,
GeocoderOptions,
GeocodingCallback,
geocodingParams,
GeocodingResult,
reverseParams
} from './api';
export interface ArcGisOptions extends GeocoderOptions {}
/**
* Implementation of the [ArcGIS geocoder](https://developers.arcgis.com/features/geocoding/)
*/
export class ArcGis implements IGeocoder {
options: ArcGisOptions = {
serviceUrl: 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer',
apiKey: ''
};
constructor(options?: Partial<ArcGisOptions>) {
L.Util.setOptions(this, options);
}
geocode(query: string, cb: GeocodingCallback, context?: any): void {
const params = geocodingParams(this.options, {
token: this.options.apiKey,
SingleLine: query,
outFields: 'Addr_Type',
forStorage: false,
maxLocations: 10,
f: 'json'
});
getJSON(this.options.serviceUrl + '/findAddressCandidates', params, data => {
const results: GeocodingResult[] = [];
if (data.candidates && data.candidates.length) {
for (let i = 0; i <= data.candidates.length - 1; i++) {
const loc = data.candidates[i];
const latLng = L.latLng(loc.location.y, loc.location.x);
const latLngBounds = L.latLngBounds(
L.latLng(loc.extent.ymax, loc.extent.xmax),
L.latLng(loc.extent.ymin, loc.extent.xmin)
);
results[i] = {
name: loc.address,
bbox: latLngBounds,
center: latLng
};
}
}
cb.call(context, results);
});
}
suggest(query: string, cb: GeocodingCallback, context?: any): void {
return this.geocode(query, cb, context);
}
reverse(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void {
const params = reverseParams(this.options, {
location: location.lng + ',' + location.lat,
distance: 100,
f: 'json'
});
getJSON(this.options.serviceUrl + '/reverseGeocode', params, data => {
const result: GeocodingResult[] = [];
if (data && !data.error) {
const center = L.latLng(data.location.y, data.location.x);
const bbox = L.latLngBounds(center, center);
result.push({
name: data.address.Match_addr,
center: center,
bbox: bbox
});
}
cb.call(context, result);
});
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link ArcGis}
* @param options the options
*/
export function arcgis(options?: Partial<ArcGisOptions>) {
return new ArcGis(options);
}
import * as L from 'leaflet';
import { jsonp } from '../util';
import {
IGeocoder,
GeocoderOptions,
GeocodingCallback,
geocodingParams,
GeocodingResult,
reverseParams
} from './api';
export interface BingOptions extends GeocoderOptions {}
/**
* Implementation of the [Bing Locations API](https://docs.microsoft.com/en-us/bingmaps/rest-services/locations/)
*/
export class Bing implements IGeocoder {
options: BingOptions = {
serviceUrl: 'https://dev.virtualearth.net/REST/v1/Locations'
};
constructor(options?: Partial<BingOptions>) {
L.Util.setOptions(this, options);
}
geocode(query: string, cb: GeocodingCallback, context?: any): void {
const params = geocodingParams(this.options, {
query: query,
key: this.options.apiKey
});
jsonp(
this.options.apiKey,
params,
data => {
const results: GeocodingResult[] = [];
if (data.resourceSets.length > 0) {
for (let i = data.resourceSets[0].resources.length - 1; i >= 0; i--) {
const resource = data.resourceSets[0].resources[i],
bbox = resource.bbox;
results[i] = {
name: resource.name,
bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]),
center: L.latLng(resource.point.coordinates)
};
}
}
cb.call(context, results);
},
this,
'jsonp'
);
}
reverse(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void {
const params = reverseParams(this.options, {
key: this.options.apiKey
});
jsonp(
this.options.serviceUrl + location.lat + ',' + location.lng,
params,
data => {
const results: GeocodingResult[] = [];
for (let i = data.resourceSets[0].resources.length - 1; i >= 0; i--) {
const resource = data.resourceSets[0].resources[i],
bbox = resource.bbox;
results[i] = {
name: resource.name,
bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]),
center: L.latLng(resource.point.coordinates)
};
}
cb.call(context, results);
},
this,
'jsonp'
);
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Bing}
* @param options the options
*/
export function bing(options?: Partial<BingOptions>) {
return new Bing(options);
}
import * as L from 'leaflet';
import { getJSON } from '../util';
import {
IGeocoder,
GeocoderOptions,
GeocodingCallback,
geocodingParams,
GeocodingResult,
reverseParams
} from './api';
/**
* Implementation of the [Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/)
*/
export interface GoogleOptions extends GeocoderOptions {}
export class Google implements IGeocoder {
options: GoogleOptions = {
serviceUrl: 'https://maps.googleapis.com/maps/api/geocode/json'
};
constructor(options?: Partial<GoogleOptions>) {
L.Util.setOptions(this, options);
}
geocode(query: string, cb: GeocodingCallback, context?: any): void {
const params = geocodingParams(this.options, {
key: this.options.apiKey,
address: query
});
getJSON(this.options.serviceUrl, params, data => {
const results: GeocodingResult[] = [];
if (data.results && data.results.length) {
for (let i = 0; i <= data.results.length - 1; i++) {
const loc = data.results[i];
const latLng = L.latLng(loc.geometry.location);
const latLngBounds = L.latLngBounds(
L.latLng(loc.geometry.viewport.northeast),
L.latLng(loc.geometry.viewport.southwest)
);
results[i] = {
name: loc.formatted_address,
bbox: latLngBounds,
center: latLng,
properties: loc.address_components
};
}
}
cb.call(context, results);
});
}
reverse(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void {
const params = reverseParams(this.options, {
key: this.options.apiKey,
latlng: location.lat + ',' + location.lng
});
getJSON(this.options.serviceUrl, params, data => {
const results: GeocodingResult[] = [];
if (data.results && data.results.length) {
for (let i = 0; i <= data.results.length - 1; i++) {
const loc = data.results[i];
const center = L.latLng(loc.geometry.location);
const bbox = L.latLngBounds(
L.latLng(loc.geometry.viewport.northeast),
L.latLng(loc.geometry.viewport.southwest)
);
results[i] = {
name: loc.formatted_address,
bbox: bbox,
center: center,
properties: loc.address_components
};
}
}
cb.call(context, results);
});
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Google}
* @param options the options
*/
export function google(options?: Partial<GoogleOptions>) {
return new Google(options);
}
import * as L from 'leaflet';
import { getJSON } from '../util';
import {
IGeocoder,
GeocoderOptions,
GeocodingCallback,
geocodingParams,
GeocodingResult,
reverseParams
} from './api';
export interface HereOptions extends GeocoderOptions {
/**
* Use `apiKey` and the new `HEREv2` geocoder
* @deprecated
*/
app_id: string;
/**
* Use `apiKey` and the new `HEREv2` geocoder
* @deprecated
*/
app_code: string;
reverseGeocodeProxRadius?: any;
apiKey: string;
maxResults: number;
}
/**
* Implementation of the [HERE Geocoder API](https://developer.here.com/documentation/geocoder/topics/introduction.html)
*/
export class HERE implements IGeocoder {
options: HereOptions = {
serviceUrl: 'https://geocoder.api.here.com/6.2/',
app_id: '',
app_code: '',
apiKey: '',
maxResults: 5
};
constructor(options?: Partial<HereOptions>) {
L.Util.setOptions(this, options);
if (options.apiKey) throw Error('apiKey is not supported, use app_id/app_code instead!');
}
geocode(query: string, cb: GeocodingCallback, context?: any): void {
const params = geocodingParams(this.options, {
searchtext: query,
gen: 9,
app_id: this.options.app_id,
app_code: this.options.app_code,
jsonattributes: 1,
maxresults: this.options.maxResults
});
this.getJSON(this.options.serviceUrl + 'geocode.json', params, cb, context);
}
reverse(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void {
let prox = location.lat + ',' + location.lng;
if (this.options.reverseGeocodeProxRadius) {
prox += ',' + this.options.reverseGeocodeProxRadius;
}
const params = reverseParams(this.options, {
prox,
mode: 'retrieveAddresses',
app_id: this.options.app_id,
app_code: this.options.app_code,
gen: 9,
jsonattributes: 1,
maxresults: this.options.maxResults
});
this.getJSON(this.options.serviceUrl + 'reversegeocode.json', params, cb, context);
}
getJSON(url: string, params: any, cb: GeocodingCallback, context?: any) {
getJSON(url, params, data => {
const results: GeocodingResult[] = [];
if (data.response.view && data.response.view.length) {
for (let i = 0; i <= data.response.view[0].result.length - 1; i++) {
const loc = data.response.view[0].result[i].location;
const center = L.latLng(loc.displayPosition.latitude, loc.displayPosition.longitude);
const bbox = L.latLngBounds(
L.latLng(loc.mapView.topLeft.latitude, loc.mapView.topLeft.longitude),
L.latLng(loc.mapView.bottomRight.latitude, loc.mapView.bottomRight.longitude)
);
results[i] = {
name: loc.address.label,
properties: loc.address,
bbox: bbox,
center: center
};
}
}
cb.call(context, results);
});
}
}
/**
* Implementation of the new [HERE Geocoder API](https://developer.here.com/documentation/geocoding-search-api/api-reference-swagger.html)
*/
export class HEREv2 implements IGeocoder {
options: HereOptions = {
serviceUrl: 'https://geocode.search.hereapi.com/v1',
apiKey: '',
app_id: '',
app_code: '',
maxResults: 10
};
constructor(options?: Partial<HereOptions>) {
L.Util.setOptions(this, options);
}
geocode(query: string, cb: GeocodingCallback, context?: any): void {
const params = geocodingParams(this.options, {
q: query,
apiKey: this.options.apiKey,
limit: this.options.maxResults
});
if (!params.at && !params.in) {
throw Error(
'at / in parameters not found. Please define coordinates (at=latitude,longitude) or other (in) in your geocodingQueryParams.'
);
}
this.getJSON(this.options.serviceUrl + '/discover', params, cb, context);
}
reverse(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void {
const params = reverseParams(this.options, {
at: location.lat + ',' + location.lng,
limit: this.options.reverseGeocodeProxRadius,
apiKey: this.options.apiKey
});
this.getJSON(this.options.serviceUrl + '/revgeocode', params, cb, context);
}
getJSON(url: string, params: any, cb: GeocodingCallback, context?: any) {
getJSON(url, params, data => {
const results: GeocodingResult[] = [];
if (data.items && data.items.length) {
for (let i = 0; i <= data.items.length - 1; i++) {
const item = data.items[i];
const latLng = L.latLng(item.position.lat, item.position.lng);
let bbox: L.LatLngBounds;
if (item.mapView) {
bbox = L.latLngBounds(
L.latLng(item.mapView.south, item.mapView.west),
L.latLng(item.mapView.north, item.mapView.east)
);
} else {
// Using only position when not provided
bbox = L.latLngBounds(
L.latLng(item.position.lat, item.position.lng),
L.latLng(item.position.lat, item.position.lng)
);
}
results[i] = {
name: item.address.label,
properties: item.address,
bbox: bbox,
center: latLng
};
}
}
cb.call(context, results);
});
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link HERE}
* @param options the options
*/
export function here(options?: Partial<HereOptions>) {
if (options.apiKey) {
return new HEREv2(options);
} else {
return new HERE(options);
}
}
export * from './api';
export * from './arcgis';
export * from './bing';
export * from './google';
export * from './here';
export * from './latlng';
export * from './mapbox';
export * from './mapquest';
export * from './neutrino';
export * from './nominatim';
export * from './open-location-code';
export * from './opencage';
export * from './pelias';
export * from './photon';
export * from './what3words';
import * as L from 'leaflet';
import { IGeocoder, GeocodingCallback, GeocodingResult } from './api';
export interface LatLngOptions {
/**
* The next geocoder to use for non-supported queries
*/
next?: IGeocoder;
/**
* The size in meters used for passing to `LatLng.toBounds`
*/
sizeInMeters: number;
}
/**
* Parses basic latitude/longitude strings such as `'50.06773 14.37742'`, `'N50.06773 W14.37742'`, `'S 50° 04.064 E 014° 22.645'`, or `'S 50° 4′ 03.828″, W 14° 22′ 38.712″'`
* @param query the latitude/longitude string to parse
* @returns the parsed latitude/longitude
*/
export function parseLatLng(query: string): L.LatLng | undefined {
let match;
// regex from https://github.com/openstreetmap/openstreetmap-website/blob/master/app/controllers/geocoder_controller.rb
if ((match = query.match(/^([NS])\s*(\d{1,3}(?:\.\d*)?)\W*([EW])\s*(\d{1,3}(?:\.\d*)?)$/))) {
// [NSEW] decimal degrees
return L.latLng(
(/N/i.test(match[1]) ? 1 : -1) * +match[2],
(/E/i.test(match[3]) ? 1 : -1) * +match[4]
);
} else if (
(match = query.match(/^(\d{1,3}(?:\.\d*)?)\s*([NS])\W*(\d{1,3}(?:\.\d*)?)\s*([EW])$/))
) {
// decimal degrees [NSEW]
return L.latLng(
(/N/i.test(match[2]) ? 1 : -1) * +match[1],
(/E/i.test(match[4]) ? 1 : -1) * +match[3]
);
} else if (
(match = query.match(
/^([NS])\s*(\d{1,3})°?\s*(\d{1,3}(?:\.\d*)?)?['′]?\W*([EW])\s*(\d{1,3})°?\s*(\d{1,3}(?:\.\d*)?)?['′]?$/
))
) {
// [NSEW] degrees, decimal minutes
return L.latLng(
(/N/i.test(match[1]) ? 1 : -1) * (+match[2] + +match[3] / 60),
(/E/i.test(match[4]) ? 1 : -1) * (+match[5] + +match[6] / 60)
);
} else if (
(match = query.match(
/^(\d{1,3})°?\s*(\d{1,3}(?:\.\d*)?)?['′]?\s*([NS])\W*(\d{1,3})°?\s*(\d{1,3}(?:\.\d*)?)?['′]?\s*([EW])$/
))
) {
// degrees, decimal minutes [NSEW]
return L.latLng(
(/N/i.test(match[3]) ? 1 : -1) * (+match[1] + +match[2] / 60),
(/E/i.test(match[6]) ? 1 : -1) * (+match[4] + +match[5] / 60)
);
} else if (
(match = query.match(
/^([NS])\s*(\d{1,3})°?\s*(\d{1,2})['′]?\s*(\d{1,3}(?:\.\d*)?)?["″]?\W*([EW])\s*(\d{1,3})°?\s*(\d{1,2})['′]?\s*(\d{1,3}(?:\.\d*)?)?["″]?$/
))
) {
// [NSEW] degrees, minutes, decimal seconds
return L.latLng(
(/N/i.test(match[1]) ? 1 : -1) * (+match[2] + +match[3] / 60 + +match[4] / 3600),
(/E/i.test(match[5]) ? 1 : -1) * (+match[6] + +match[7] / 60 + +match[8] / 3600)
);
} else if (
(match = query.match(
/^(\d{1,3})°?\s*(\d{1,2})['′]?\s*(\d{1,3}(?:\.\d*)?)?["″]\s*([NS])\W*(\d{1,3})°?\s*(\d{1,2})['′]?\s*(\d{1,3}(?:\.\d*)?)?["″]?\s*([EW])$/
))
) {
// degrees, minutes, decimal seconds [NSEW]
return L.latLng(
(/N/i.test(match[4]) ? 1 : -1) * (+match[1] + +match[2] / 60 + +match[3] / 3600),
(/E/i.test(match[8]) ? 1 : -1) * (+match[5] + +match[6] / 60 + +match[7] / 3600)
);
} else if ((match = query.match(/^\s*([+-]?\d+(?:\.\d*)?)\s*[\s,]\s*([+-]?\d+(?:\.\d*)?)\s*$/))) {
return L.latLng(+match[1], +match[2]);
}
}
/**
* Parses basic latitude/longitude strings such as `'50.06773 14.37742'`, `'N50.06773 W14.37742'`, `'S 50° 04.064 E 014° 22.645'`, or `'S 50° 4′ 03.828″, W 14° 22′ 38.712″'`
*/
export class LatLng implements IGeocoder {
options: LatLngOptions = {
next: undefined,
sizeInMeters: 10000
};
constructor(options?: Partial<LatLngOptions>) {
L.Util.setOptions(this, options);
}
geocode(query: string, cb: GeocodingCallback, context?: any) {
const center = parseLatLng(query);
if (center) {
const results: GeocodingResult[] = [
{
name: query,
center: center,
bbox: center.toBounds(this.options.sizeInMeters)
}
];
cb.call(context, results);
} else if (this.options.next) {
this.options.next.geocode(query, cb, context);
}
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link LatLng}
* @param options the options
*/
export function latLng(options?: Partial<LatLngOptions>) {
return new LatLng(options);
}
import * as L from 'leaflet';
import { getJSON } from '../util';
import {
IGeocoder,
GeocoderOptions,
GeocodingCallback,
geocodingParams,
GeocodingResult,
reverseParams
} from './api';
export interface MapboxOptions extends GeocoderOptions {}
/**
* Implementation of the [Mapbox Geocoding](https://www.mapbox.com/api-documentation/#geocoding)
*/
export class Mapbox implements IGeocoder {
options: MapboxOptions = {
serviceUrl: 'https://api.mapbox.com/geocoding/v5/mapbox.places/'
};
constructor(options?: Partial<MapboxOptions>) {
L.Util.setOptions(this, options);
}
_getProperties(loc) {
const properties = {
text: loc.text,
address: loc.address
};
for (let j = 0; j < (loc.context || []).length; j++) {
const id = loc.context[j].id.split('.')[0];
properties[id] = loc.context[j].text;
// Get country code when available
if (loc.context[j].short_code) {
properties['countryShortCode'] = loc.context[j].short_code;
}
}
return properties;
}
geocode(query: string, cb: GeocodingCallback, context?: any): void {
const params: any = geocodingParams(this.options, {
access_token: this.options.apiKey
});
if (
params.proximity !== undefined &&
params.proximity.lat !== undefined &&
params.proximity.lng !== undefined
) {
params.proximity = params.proximity.lng + ',' + params.proximity.lat;
}
getJSON(this.options.serviceUrl + encodeURIComponent(query) + '.json', params, data => {
const results: GeocodingResult[] = [];
if (data.features && data.features.length) {
for (let i = 0; i <= data.features.length - 1; i++) {
const loc = data.features[i];
const center = L.latLng(loc.center.reverse());
let bbox: L.LatLngBounds;
if (loc.bbox) {
bbox = L.latLngBounds(
L.latLng(loc.bbox.slice(0, 2).reverse()),
L.latLng(loc.bbox.slice(2, 4).reverse())
);
} else {
bbox = L.latLngBounds(center, center);
}
results[i] = {
name: loc.place_name,
bbox: bbox,
center: center,
properties: this._getProperties(loc)
};
}
}
cb.call(context, results);
});
}
suggest(query: string, cb: GeocodingCallback, context?: any): void {
return this.geocode(query, cb, context);
}
reverse(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void {
const url = this.options.serviceUrl + location.lng + ',' + location.lat + '.json';
const param = reverseParams(this.options, {
access_token: this.options.apiKey
});
getJSON(url, param, data => {
const results: GeocodingResult[] = [];
if (data.features && data.features.length) {
for (let i = 0; i <= data.features.length - 1; i++) {
const loc = data.features[i];
const center = L.latLng(loc.center.reverse());
let bbox: L.LatLngBounds;
if (loc.bbox) {
bbox = L.latLngBounds(
L.latLng(loc.bbox.slice(0, 2).reverse()),
L.latLng(loc.bbox.slice(2, 4).reverse())
);
} else {
bbox = L.latLngBounds(center, center);
}
results[i] = {
name: loc.place_name,
bbox: bbox,
center: center,
properties: this._getProperties(loc)
};
}
}
cb.call(context, results);
});
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Mapbox}
* @param options the options
*/
export function mapbox(options?: Partial<MapboxOptions>) {
return new Mapbox(options);
}
import * as L from 'leaflet';
import { getJSON } from '../util';
import {
IGeocoder,
GeocoderOptions,
GeocodingCallback,
geocodingParams,
GeocodingResult,
reverseParams
} from './api';
export interface MapQuestOptions extends GeocoderOptions {}
/**
* Implementation of the [MapQuest Geocoding API](http://developer.mapquest.com/web/products/dev-services/geocoding-ws)
*/
export class MapQuest implements IGeocoder {
options: MapQuestOptions = {
serviceUrl: 'https://www.mapquestapi.com/geocoding/v1'
};
constructor(options?: Partial<MapQuestOptions>) {
L.Util.setOptions(this, options);
// MapQuest seems to provide URI encoded API keys,
// so to avoid encoding them twice, we decode them here
this.options.apiKey = decodeURIComponent(this.options.apiKey);
}
_formatName(...parts: string[]) {
return parts.filter(s => !!s).join(', ');
}
geocode(query: string, cb: GeocodingCallback, context?: any): void {
const params = geocodingParams(this.options, {
key: this.options.apiKey,
location: query,
limit: 5,
outFormat: 'json'
});
getJSON(
this.options.serviceUrl + '/address',
params,
L.Util.bind(function(data) {
const results: GeocodingResult[] = [];
if (data.results && data.results[0].locations) {
for (let i = data.results[0].locations.length - 1; i >= 0; i--) {
const loc = data.results[0].locations[i];
const center = L.latLng(loc.latLng);
results[i] = {
name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1),
bbox: L.latLngBounds(center, center),
center: center
};
}
}
cb.call(context, results);
}, this)
);
}
reverse(location: L.LatLngLiteral, scale: number, cb: GeocodingCallback, context?: any): void {
const params = reverseParams(this.options, {
key: this.options.apiKey,
location: location.lat + ',' + location.lng,
outputFormat: 'json'
});
getJSON(
this.options.serviceUrl + '/reverse',
params,
L.Util.bind(function(data) {
const results: GeocodingResult[] = [];
if (data.results && data.results[0].locations) {
for (let i = data.results[0].locations.length - 1; i >= 0; i--) {
const loc = data.results[0].locations[i];
const center = L.latLng(loc.latLng);
results[i] = {
name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1),
bbox: L.latLngBounds(center, center),
center: center
};
}
}
cb.call(context, results);
}, this)
);
}
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link MapQuest}
* @param options the options
*/
export function mapQuest(options?: Partial<MapQuestOptions>) {
return new MapQuest(options);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment