/**
* @namespace Services
* @category Services
* @module geocoding-service
* */
import api from '@/api'
import { getEnvValue } from '@/services/env-service.js'
import { getNestedValue } from '@/utils/object'
import { formatSeconds } from '@/utils/dates'
import i18n from '@/i18n'
import localforage from 'localforage'
import APIUrls from '@/config/simpliciti-apis.js'
const R = require('ramda')
const autocompleteDefaultProvider = 'osm'
const autocompleteProvidersConfiguration = {
osm: {
url:
process.env.VUE_APP_GEOCODING_PROVIDER_OSM_TEST_URL ||
'https://osm-geocoding.geosab.eu',
},
}
export const autocompleteProviders = [
'osm',
'simpliciti_v3',
'simpliciti_v3_google',
]
export const transportTypeOptions = [
{ value: 'car', text: 'geocoding.routing.transport_type.car' },
{ value: 'truck', text: 'geocoding.routing.transport_type.truck' },
]
/**
* @param placeIdOrPlaceItem
* @param {*} options i.g {provider:"simpliciti_v3"}
* https://nominatim.org/release-docs/latest/api/Overview/
* options.provider="simpliciti_v3" will use OSM proxied though our servers
*/
export async function autocompleteFetchPlaceDetails(
placeIdOrPlaceItem,
options = {}
) {
let addressComponent
let placeId = placeIdOrPlaceItem.code || placeIdOrPlaceItem
options.provider = options.provider || autocompleteDefaultProvider
if (!autocompleteProviders.includes(options.provider)) {
throw new Error('INVALID_GEOCODING_AUTOCOMPLETE_PROVIDER')
}
let query = `/details?place_id=${placeId}&format=json&addressdetails=1`
let r = null
if (options.provider === 'simpliciti_v3') {
r = (
await api.v3.post(APIUrls.APIV3_CARTO_GEOCODING_PROXY_OSM_NOMINATIM, {
query,
})
).data
} else if (options.provider === 'simpliciti_v3_google') {
query = `/geocode/json?place_id=${placeId}`
r = (
await api.v3.post(`${APIUrls.APIV3_CARTO_GEOCODING_PROXY_GOOGLE}`, {
query,
})
).data
} else {
r = fetch(
`${autocompleteProvidersConfiguration[options.provider]?.url}${query}`
)
r = await r
r = await r.json()
}
if (options.provider === 'simpliciti_v3_google') {
return formateGoogleResult(r.results)
} else {
addressComponent = (type) =>
(r.address.find((o) => o.type === type) || {}).localname || ''
if (!(placeIdOrPlaceItem.name || options.formatted)) {
throw new Error(
`Expected to compute 'formatted' though options.formatted or placeIdOrPlaceItem.name`
)
}
}
return {
formatted: placeIdOrPlaceItem.name || options.formatted || `)`,
streetNumber: addressComponent('house_number'),
street: addressComponent('residential'),
neighbourhood: addressComponent('neighbourhood'),
suburb: addressComponent('suburb'),
city: addressComponent('city'),
administrative: addressComponent('administrative'),
state: addressComponent('state'),
postcode: addressComponent('postcode'),
country: addressComponent('country'),
countryCode: addressComponent('country_code'),
lng: r.geometry && r.geometry.coordinates && r.geometry.coordinates[0],
lat: r.geometry && r.geometry.coordinates && r.geometry.coordinates[1],
}
}
function googleAddressComponentsHasKey(result, key) {
if (Object.getOwnPropertyDescriptor(result, 'address_components')) {
let data = result.address_components.filter((element) =>
element.types.includes(key)
)
if (data.length) {
return data[0].long_name
}
return ''
}
}
function formateGoogleResult(results) {
if (Array.isArray(results) && results.length) {
const result = results[0]
return {
formatted: result.formatted_address || '',
streetNumber: googleAddressComponentsHasKey(result, 'street_number'),
street: googleAddressComponentsHasKey(result, 'route'),
neighbourhood: googleAddressComponentsHasKey(result, 'administrative'),
suburb: googleAddressComponentsHasKey(result, 'suburb'),
city: googleAddressComponentsHasKey(result, 'locality'),
administrative: googleAddressComponentsHasKey(
result,
'administrative_area_level_1'
),
state: googleAddressComponentsHasKey(
result,
'administrative_area_level_2'
),
postcode: googleAddressComponentsHasKey(result, 'postal_code'),
country: googleAddressComponentsHasKey(result, 'country'),
country_code: googleAddressComponentsHasKey(result, 'country_code'),
lng:
result.geometry &&
result.geometry.location &&
result.geometry.location.lng,
lat:
result.geometry &&
result.geometry.location &&
result.geometry.location.lat,
}
}
}
/**
*
* @param {*} query i.g 129 Rue Andy Warhol
* @param {*} options i.g {provider:"simpliciti_v3"}
* https://nominatim.org/release-docs/latest/api/Overview/
* options.provider="simpliciti_v3" will use OSM proxied though our servers
*/
export async function autocompleteFetchPlaces(query, options = {}) {
let userCountry = (await localforage.getItem('user_infos')).country || ''
options.provider = options.provider || autocompleteDefaultProvider
if (!autocompleteProviders.includes(options.provider)) {
throw new Error('INVALID_GEOCODING_AUTOCOMPLETE_PROVIDER')
}
let r
let requestQuery = `/?accept-language=${userCountry.toLowerCase()}&q=${query}&format=json&countrycodes=${countryCodes.join(
','
)}`
if (options.provider === 'simpliciti_v3') {
r = (
await api.v3.post(APIUrls.APIV3_CARTO_GEOCODING_PROXY_OSM_NOMINATIM, {
query: requestQuery,
})
).data
} else if (options.provider === 'simpliciti_v3_google') {
requestQuery = `/place/autocomplete/json?input=${query}&language=${userCountry.toLowerCase()}&components=country:${userCountry.toLowerCase()}`
r = (
await api.v3.post(`${APIUrls.APIV3_CARTO_GEOCODING_PROXY_GOOGLE}`, {
query: requestQuery,
})
).data.predictions
} else {
r = fetch(
`${
autocompleteProvidersConfiguration[options.provider]?.url
}${requestQuery}`
)
r = await r
r = await r.json()
}
return r.map((i) => ({
name: i.display_name || i.description,
code: i.place_id,
osmType: i.osm_type || null,
osmId: i.osm_id || null,
}))
}
/**
* If basemapId is empty (e.g use case for custom base maps such as OSM), do not send basemapId parameter to server (This will avoid API 500 error)
* @param {*} basemapId
* @returns
*/
function getBasemapIdParam(basemapId) {
if (basemapId) {
return {
basemapId,
}
} else {
return {}
}
}
/**
* API for locate address (form/free mode)
*
* @param {*} basemapId
* @param {*} options
* @returns
*/
export async function forwardGeocoding(basemapId, options = {}) {
options.getRawResponse =
options.getRawResponse === undefined ? false : options.getRawResponse
if (options.address) {
return api.v3.post(APIUrls.APIV3_CARTO_GEOCODING_NATURAL, {
...getBasemapIdParam(basemapId),
...R.pick(['getRawResponse', 'address'], options),
})
} else {
return (
await api.v3.post(APIUrls.APIV3_CARTO_GEOCODING, {
...getBasemapIdParam(basemapId),
...R.pick(
[
'streetNumber',
'streetName',
'city',
'zipCode',
'county',
'region',
'country',
'getRawResponse',
],
options
),
})
).data
}
}
/**
*
* API for locate address by gps coords
* *Les coordonnées (longitude, latitude) au format WGS84
* @param {*} basemapId
* @param {*} options
* @returns
*/
export async function reverseGeocoding(basemapId, options = {}) {
options.latitude = options.lat || options.latitude
options.longitude = options.lng || options.longitude
options.getRawResponse =
options.getRawResponse === undefined ? false : options.getRawResponse
return (
await api.v3.post(APIUrls.APIV3_CARTO_GEOCODING_REVERSE, {
...getBasemapIdParam(basemapId),
...R.pick(['longitude', 'latitude', 'getRawResponse'], options),
})
).data
}
/**
*
* API for routing feature (Calcul iti)
* *Les coordonnées (longitude, latitude) au format WGS84
* @param {*} basemapId
* @param {*} options
* @returns
*/
export async function routingGeocoding(basemapId, options = {}) {
options.getRawResponse =
options.getRawResponse === undefined ? false : options.getRawResponse
let res = await api.v3.post(APIUrls.APIV3_CARTO_ROUTING, {
...getBasemapIdParam(basemapId),
...R.pick(
[
'coordinates',
'transportType',
'shortest',
'fastest',
'avoidFerries',
'avoidMotorways',
'avoidTolls',
'vehicle',
'speedPonderation',
'geocoderOptions',
'getRawResponse',
],
options
),
})
res = res.data
let instructions = (res.instructions || [])
.map((item, index) =>
normalizeInstructionItem(item, index, res.instructions)
)
.filter((item) => {
return !!item && item.coordinates.length > 0
})
let getMarkerType = (item) => {
let index = (options.coordinates || []).findIndex(
(coords) => coords[0] == item.longitude && coords[1] == item.latitude
)
if (index === 0) return 'start'
if (index === (options.coordinates || []).length - 1) return 'end'
return 'step'
}
let markers = res.viaCoordinates
.filter((item) => item.used)
.map((item) => ({
lat: getNestedValue(item, 'latitudeMapmatch'),
lng: getNestedValue(item, 'longitudeMapmatch'),
type: getMarkerType(item),
}))
return {
markers,
instructions,
instructionsCoordinates: getNestedValue(res, 'linestring.coordinates', []),
lengthFormatted:
Math.round(getNestedValue(res, 'length', 0) / 1000, 1) + ' km',
durationFormatted: formatSeconds(getNestedValue(res, 'duration', 0)),
averageSpeedFormatted: getNestedValue(res, 'averageSpeed', 0) + ' km/h',
}
}
/**
* @description
* Copied from Geored RoutingHtmlDumper.php
* @todo Add unit-test
*/
function normalizeInstructionItem(item, itemIndex, instructions) {
let duration = 0
instructions.forEach((instructionItem, instructionItemIndex) => {
if (instructionItemIndex <= itemIndex) {
duration = duration + instructionItem.duration
} else {
return false
}
})
let durationFormatted = formatSeconds(duration)
let lengthFormatted
if (item.length > 1000) {
lengthFormatted = Math.round(item.length / 1000, 1) + ' km'
} else {
lengthFormatted = item.length + ' m'
}
let nextRoadText = ''
if (item.nextRoadSign) {
nextRoadText =
i18n.t('geocoding.instructions.DIRECTION') +
' <b>' +
item.nextRoadSign +
'</b>'
} else if (item.nextRoadNumber) {
nextRoadText =
i18n.t('geocoding.instructions.ON_ROAD') +
' <b>' +
item.nextRoadNumber +
'</b>'
} else if (item.nextRoadName) {
nextRoadText =
i18n.t('geocoding.instructions.ON_ROAD') +
' <b>' +
item.nextRoadName +
'</b>'
} else if (item.nextStep) {
nextRoadText =
i18n.t('geocoding.instructions.DIRECTION') +
' <b>' +
item.nextStep +
'</b>'
}
let whenText = ''
if (item.maneuver == 'STRAIGHT' || item.maneuver == 'UNKNOWN') {
whenText = i18n.t('geocoding.instructions.DURING')
} else {
whenText = i18n.t('geocoding.instructions.IN')
}
let instructionIconCode = ''
let instructionText
let prevInstruction = instructions[itemIndex - 1]
? instructions[itemIndex - 1]
: null
let nextInstruction = instructions[itemIndex + 1]
? instructions[itemIndex + 1]
: {
subManeuver: '',
}
if (
!!instructions[itemIndex + 1] && //only one instruction for an ENTER-EXIT
((item.maneuver == 'ROUNDABOUT_ENTER' &&
nextInstruction.maneuver == 'ROUNDABOUT_EXIT') ||
(item.subManeuver == 'ROUNDABOUT_ENTER' &&
nextInstruction.subManeuver == 'ROUNDABOUT_EXIT'))
) {
return null
} else if (
!!prevInstruction && //only one instruction for an ENTER-EXIT
((prevInstruction.maneuver == 'ROUNDABOUT_ENTER' &&
item.maneuver == 'ROUNDABOUT_EXIT') ||
(prevInstruction.subManeuver == 'ROUNDABOUT_ENTER' &&
item.subManeuver == 'ROUNDABOUT_EXIT'))
) {
instructionIconCode = 'ROUNDABOUT_' + item.roundaboutExit
let lengthRA = ''
if (prevInstruction.length + item.length >= 1000) {
lengthRA =
Math.round((prevInstruction.length + item.length) / 1000, 1) + ' km'
} else {
lengthRA = prevInstruction.length + item.length + ' m'
}
instructionText =
i18n.t('geocoding.instructions.AT') + ' ' + lengthRA + ', '
instructionText += i18n.t('geocoding.instructions.ROUNDABOUT_ENTER') + ' '
instructionText +=
' ' +
i18n.t('geocoding.instructions.ROUNDABOUT_EXIT') +
' ' +
item.roundaboutExit +
i18n.t('geocoding.instructions.ROUNDABOUT_EXIT_NUMBER')
instructionText += ' ' + nextRoadText
let duration_to_nextRA = prevInstruction.duration + item.duration
instructionText +=
duration_to_nextRA < 60
? ' '
: ' ' + whenText + ' ' + formatSeconds(duration_to_nextRA) + '.'
} else if (
item.maneuver == 'ROUNDABOUT' ||
item.maneuver == 'ROUNDABOUT_ENTER' ||
item.subManeuver == 'ROUNDABOUT' ||
item.subManeuver == 'ROUNDABOUT_ENTER'
) {
instructionIconCode = 'ROUNDABOUT_'.item.roundaboutExit
instructionText =
i18n.t('geocoding.instructions.AT') + ' ' + lengthFormatted + ', '
instructionText += i18n.t('geocoding.instructions.ROUNDABOUT_ENTER') + ' '
instructionText +=
' ' +
i18n.t('geocoding.instructions.ROUNDABOUT_EXIT') +
' ' +
item.roundaboutExit +
i18n.t('geocoding.instructions.ROUNDABOUT_EXIT_NUMBER')
instructionText += ' '.nextRoadText
instructionText +=
item.duration < 60
? ' '
: ' ' +
i18n.t('geocoding.instructions.IN') +
' ' +
formatSeconds(item.duration) +
'.'
} else if (
item.maneuver == 'ROUNDABOUT_EXIT' ||
item.subManeuver == 'ROUNDABOUT_EXIT'
) {
//instructionIconCode = "ROUNDABOUT_".item.roundaboutExit;
instructionIconCode = 'ROUNDABOUT_EXIT_' + item.roundaboutExit
instructionText =
i18n.t('geocoding.instructions.AT') + ' ' + lengthFormatted + ', '
instructionText +=
i18n.t('geocoding.instructions.ROUNDABOUT_EXIT') +
' ' +
i18n.t('geocoding.instructions.AT_EXIT') +
' ' +
item.roundaboutExit +
i18n.t('geocoding.instructions.ROUNDABOUT_EXIT_NUMBER')
instructionText += ' '.nextRoadText
} else if (item.maneuver == 'STOP' || item.subManeuver == 'STOP') {
instructionText =
i18n.t('geocoding.instructions.AT') + ' ' + lengthFormatted + ', '
instructionText += i18n.t('geocoding.instructions.STOP') + ' '
instructionText +=
item.duration < 60
? ' '
: ' ' + whenText + ' ' + formatSeconds(item.duration) + '.'
} else {
instructionIconCode = item.maneuver
instructionText =
i18n.t('geocoding.instructions.AT') + ' ' + lengthFormatted + ', '
instructionText += i18n.t(`geocoding.instructions.${item.maneuver}`) + ' '
instructionText +=
item.subManeuver == '' || item.subManeuver == 'FOLLOW'
? ''
: i18n.t('geocoding.instructions.AND') +
' ' +
i18n.t(`geocoding.instructions.${item.subManeuver}`) +
' '
instructionText += nextRoadText
instructionText +=
item.duration < 60
? '. '
: ' ' + whenText + ' ' + formatSeconds(item.duration) + '.'
}
return {
number: itemIndex + 1,
//durationFormatted: formatSeconds(getNestedValue(item, "duration", 0)),
//duration: getNestedValue(item, "duration", 0),
duration,
durationFormatted,
//text: getNestedValue(item, "nextStep"),
text: instructionText,
maneuver: getNestedValue(item, 'maneuver'),
coordinates: getNestedValue(item, 'linestring.coordinates', []),
iconCode: instructionIconCode
.split('ROUNDABOUT_')
.join('ROUNDABOUT_EXIT_')
.split('EXIT_EXIT')
.join('EXIT'),
}
}
/**
* OSM Nominatim can be restricted to certain countries to improve search speed:
* Africa + France, Belgium, Switzerland, Germany, Netherlands
*/
const countryCodes = [
'ES',
'CH',
'BE',
'FR',
'NL',
'GE',
'DZ',
' AO',
' SH',
' BJ',
' BW',
' BF',
' BI',
' CM',
' CV',
' CF',
' TD',
' KM',
' CG',
' CD',
' DJ',
' EG',
' GQ',
' ER',
' SZ',
' ET',
' GA',
' GM',
' GH',
' GN',
' GW',
' CI',
' KE',
' LS',
' LR',
' LY',
' MG',
' MW',
' ML',
' MR',
' MU',
' YT',
' MA',
' MZ',
' NA',
' NE',
' NG',
' ST',
' RE',
' RW',
' ST',
' SN',
' SC',
' SL',
' SO',
' ZA',
' SS',
' SH',
' SD',
' SZ',
' TZ',
' TG',
' TN',
' UG',
' CD',
' ZM',
' TZ',
' ZW',
]
Source