Source: tools/mapviewer-geocodage.js

/* eslint-disable prefer-promise-reject-errors */
/* eslint vars-on-top: 0, newline-after-var: 0, consistent-return:0, no-throw-literal:0, operator-linebreak: [2, "before"], camelcase:0 */

/**
 * Ensemble de fonctions utilitaires de geocodage
 * @module geocodage
 */

import * as proj from 'ol/proj'
import { convertGeojsonGeometry } from './services/projections'

/**
 * Permet de générer un identifiant unique sur les résultats de recherche
 * @return {string} Identifiant unique généré
 */
import guid from '../utils/Guid'

// import { parse } from 'csv-parse/./sync' // L'import ne fonctionne pas

/**
 * Permet d'utiliser le service de géocodage de OpenStreetMap
 * http://wiki.openstreetmap.org/wiki/Nominatim
 *
 * @param {string} address  Adresse à géocoder
 * @param {string} epsg     Systeme de projection. Par defaut, celui du service (généralement EPSG:4326)
 * @param {string} protocol Protocol de la requête. Par défaut https
 */
function geocodeOSM (address, epsg, protocol = 'http', queryKey = 'q') {
  return fetch(`${protocol}://nominatim.openstreetmap.org/search?${queryKey}=${address}&format=json&addressdetails=1`)
    .then(response => response.json())
    .then(json => ({
      type: 'FeatureCollection',
      features: json.map(item => ({
        type: 'Feature',
        id: guid(),
        geometry: {
          type: 'Point',
          coordinates: proj.transform([+item.lon, +item.lat], 'EPSG:4326', epsg),
        },
        properties: {
          type: item.type,
          label: item.display_name,
          postcode: item.address.postcode,
          city: item.address.city,
          country: item.address.country,
        },
      })),
    }))
}
/**
 * Permet d'utiliser le service de géocodage de Here
 * https://developer.here.com/rest-apis/documentation/geocoder/topics/quick-start.html
 * TODO: résoudre problème Invalid Credentials
 * @param {string} address  Adresse à géocoder
 * @param {string} epsg     Systeme de projection. Par defaut, celui du service (généralement EPSG:4326)
 * @param {string} app_code Code de l'application pour l'authentification Here
 * @param {string} app_id   Id de l'application pour l'authentification Here
 * @param {string} protocol Protocol de la requête. Par défaut https
 * @param {string} url Url de service wfs
 * @param {string} key Clé de recherche dans le wfs
 */
function geocodeHere (address, epsg, appCode, appId, protocol, queryKey = 'searchtext') {
  return fetch(`${protocol}'://geocoder.cit.api.here.com/6.2/geocode.json?${queryKey}=${address}&app_code=${appCode}&app_id=${appId}`)
    .then(response => response.json())
    .then(json => ({
      type: 'FeatureCollection',
      features: json.Response.View[0].Results.map(({ Location }) => ({
        type: 'Feature',
        id: guid(),
        geometry: {
          type: 'Point',
          coordinates: proj.transform([Location.DisplayPosition.Longitude, Location.DisplayPosition.Latitude], 'EPSG:4326', epsg),
        },
        properties: {
          type: Location.LocationType,
          label: Location.Address.Label,
          postcode: Location.Address.PostalCode,
          city: Location.Address.City,
          country: Location.Address.Country,
        },
      })),
    }))
}

/**
 * Permet d'utiliser le service de géocodage de Google
 * https://developers.google.com/maps/documentation/geocoding/intro
 * @param {string} address  Adresse à géocoder
 * @param {string} epsg     Systeme de projection. Par defaut, celui du service (généralement EPSG:4326)
 * @param {string} key      Clé d'authetification sur l'API de Google
 * @param {string} protocol Protocol de la requête. Par défaut https
 */
function geocodeGoogle (address, epsg, key, protocol, queryKey = 'address') {
  return fetch(`${protocol}://maps.googleapis.com/maps/api/geocode/json?${queryKey}=${address}&key=${key}`)
    .then(response => response.json())
    .then(json => {
      switch (json.status) {
        case 'ZERO_RESULTS':
          return []
        case 'OK':
          return json.results
        default:
          return Promise.reject()
      }
    })
    .then(json => ({
      type: 'FeatureCollection',
      features: json.map(item => ({
        type: 'Feature',
        id: guid(),
        geometry: {
          type: 'Point',
          coordinates: proj.transform([item.geometry.location.lng, item.geometry.location.lat], 'EPSG:4326', epsg),
        },
        properties: {
          type: item.types[0],
          label: item.formatted_address,
          ...item.address_components.reduce((acc, { types, long_name }) => {
            if (types.includes('postal_code')) {
              acc.postcode = long_name
            }
            if (types.includes('locality')) {
              acc.city = long_name
            }
            if (types.includes('country')) {
              acc.country = long_name
            }
            return acc
          }, {}),
        },
      })),
    }))
}

/**
 * Cette fonction ne fonctionne pas car la BAN ne comprend pas les données envoyée via le champs de formulaire
 */
async function geocodeCSVWithBAN (addresses, epsg, protocol) {
  // const csvData = ['_code', ...addresses].join('\n')
  // const formData = new FormData()
  // const blob = new Blob([csvData], { type: 'text/csv' })
  // formData.append('data', blob, 'address.csv')
  // const response = await fetch(`${protocol}://api-adresse.data.gouv.fr/search/csv`, {
  //   method: 'POST', // *GET, POST, PUT, DELETE, etc.
  //   // mode: "cors", // no-cors, *cors, same-origin
  //   cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
  //   credentials: 'same-origin', // include, *same-origin, omit
  //   headers: {
  //     'Content-Type': 'content-type: multipart/form-data',
  //     // 'Content-Type': 'application/x-www-form-urlencoded',
  //   },
  //   redirect: 'follow', // manual, *follow, error
  //   referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
  //   body: formData,
  // })
  // const csvResponse = await response.text()
  // function parse (resp) {
  //   // TODO A implémenter
  //   return []
  // }

  // const records = parse(csvResponse, {
  //   columns: true,
  //   skip_empty_lines: true,
  // })
  // const features = records.map(record => ({
  //   type: 'Feature',
  //   id: guid(),
  //   geometry: {
  //     type: 'Point',
  //     coordinates: proj.transform([record.latitude, record.longitude], 'EPSG:4326', epsg),
  //   },
  //   properties: {
  //     ...records,
  //     country: 'France',
  //   },
  // }))

  return []
}

async function searchBAN (address, epsg, protocol) {
  const response = await fetch(`${protocol}://data.geopf.fr/geocodage/search/?q=${address}`)
  const jsonResponse = await response.json()
  const features = jsonResponse.features.map(({ geometry, properties }) => ({
    type: 'Feature',
    id: guid(),
    geometry: {
      type: 'Point',
      coordinates: proj.transform(geometry.coordinates, 'EPSG:4326', epsg),
    },
    properties: {
      ...properties,
      country: 'France',
      _code: address,
    },
  }))

  return features
}

/**
 * Permet d'utiliser le service de géocodage du gouvernement français
 * https://adresse.data.gouv.fr/api/
 * INFO: Actuellement en version bêta
 * INFO: Uniquement sur des adresses situées en France
 *
 * @param {string} address  Adresse à géocoder
 * @param {string} epsg     Systeme de projection. Par defaut, celui du service (généralement EPSG:4326)
 * @param {string} protocol Protocol de la requête. Par défaut https
 */
async function geocodeBAN (address, epsg, protocol) {
  const features = Array.isArray(address)
    ? await geocodeCSVWithBAN(address, epsg, protocol)
    : await searchBAN(address, epsg, protocol)

  return {
    type: 'FeatureCollection',
    features,
  }
}

function geocodeWFS (url, queryKey, typeOfKey, address, epsg, epsgSource) {
  let filters = null
  if (Array.isArray(address)) {
    // in
    address = address.map(val => (typeOfKey === 'string' ? `'${val}'` : val)).join(',')
    filters = `${queryKey} in (${address})`
  } else {
    // Equal
    address = typeOfKey === 'string' ? `'${address}'` : address
    filters = `${queryKey}=${address}`
  }

  return fetch(`${url}&cql_filter=${filters}`)
    .then(response => response.json())
    .then(json => ({
      type: 'FeatureCollection',
      features: json.features.map(({ geometry, properties }) => ({
        type: 'Feature',
        id: guid(),
        geometry: convertGeojsonGeometry(geometry, epsgSource, epsg),
        properties,
      })),
    }))
}

/**
 * Permet de géocoder une adresse dans un system de projection données
 * @param {Object}               options Options pour la fonction
 * @param {string}               options.address  Adresse à géocoder // liste de valeur dans le cas d'un WFS
 * @param {string | undefined}   options.service  Nom du service de geoCodage (osm, here, google, ban). Par défaut osm
 * @param {string | undefined}   options.epsg     Systeme de projection. Par defaut, celui du service (généralement EPSG:4326)
 * @param {string | undefined}   options.protocol Protocol de la requête. Par défaut https
 * @param {string | undefined}   options.app_code Code de l'application pour l'authentification Here. Requis pour le service Here
 * @param {string | undefined}   options.app_id   Identifiant de l'application pour l'authentification Here. Requis pour le service Here
 * @param {string | undefined}   options.key      Cle d'authentification de l'API de Google. Requis pour le service Google
 */
const geocode = function ({
  address,
  service = 'osm',
  epsg = 'EPSG:4326',
  epsgSource,
  protocol = 'https',
  app_code,
  app_id,
  key,
  url,
  queryKey,
  typeOfKey,
}) {
  if (!address) {
    return Promise.reject('Missing parameters')
  }

  switch (service) {
    case 'osm' :
      return geocodeOSM(address, epsg, protocol, queryKey)
    case 'here' :
      return !app_code || !app_id
        ? Promise.reject('Missing parameters. Here require app_code and app_id')
        : geocodeHere(address, epsg, app_code, app_id, protocol, queryKey)
    case 'google' :
      return !key
        ? Promise.reject('Missing parameters. Google require an API key')
        : geocodeGoogle(address, epsg, key, protocol, queryKey)
    case 'ban' :
      return geocodeBAN(address, epsg, protocol, queryKey)
    case 'wfs' :
      if (!queryKey) {
        return Promise.reject('Missing parameters. WFS require an QUERY key')
      }
      return geocodeWFS(url, queryKey, typeOfKey, address, epsg, epsgSource)
  }
}

// Permet d'etendre le module
export default {
  geocode,
}