kalisio/geokoder

View on GitHub
src/routes.js

Summary

Maintainability
A
0 mins
Test Coverage
F
43%
import { fileURLToPath } from 'url'
import fs from 'fs-extra'
import path from 'path'
import _ from 'lodash'
import errors from '@feathersjs/errors'
import makeDebug from 'debug'
import { scoreResult } from './scoring.js'
import { Providers } from './providers.js'

const debug = makeDebug('geokoder:routes')

// provider
//  => liste de sources
//  => forward, reverse
//
// kano feature services = provider
//  => sources = rte-units, hubeau-stations ...

export default function (app) {
  const __filename = fileURLToPath(import.meta.url)
  const __dirname = path.dirname(__filename)
  const packageInfo = fs.readJsonSync(path.join(__dirname, '..', 'package.json'))
  const paginate = app.get('paginate')

  app.get('/healthcheck', (req, res, next) => {
    const response = {
      name: 'geokoder',
      // Allow to override version number for custom build
      version: (process.env.VERSION ? process.env.VERSION : packageInfo.version)
    }
    if (process.env.BUILD_NUMBER) {
      response.buildNumber = process.env.BUILD_NUMBER
    }
    res.json(response)
  })

  app.get('/capabilities/:operation', async (req, res, next) => {
    const operation = _.get(req, 'params.operation')
    const options = { operation }
    const all = Providers.get().filter(provider => typeof provider[operation] === 'function').map(provider => provider.capabilities(options))

    const response = {
      geocoders: [],
      i18n: app.get('i18n')['i18n']
    }
    const results = await Promise.allSettled(all)
    results.forEach((result) => {
      if (result.status !== 'fulfilled') {
        app.logger.error(result.reason.toString())
        return
      }

      response.geocoders.splice(-1, 0, ...result.value)
    })

    res.json(response)
  })

  app.get('/forward', async (req, res, next) => {
    const q = _.get(req.query, 'q')
    const filter = _.get(req.query, 'sources', '*')
    const options = { search: q, filter}
    // Some providers might support additional parameters
    if (_.has(req.query, 'limit')) options.limit = _.toInteger(_.get(req.query, 'limit'))
    else if (_.has(paginate, 'default.forward')) options.limit = _.get(paginate, 'default.forward')
    if (_.has(paginate, 'max.forward') && options.limit) options.limit = Math.min(options.limit, _.get(paginate, 'max.forward'))

    const all = Providers.get().filter(provider => typeof provider.forward === 'function').map(provider => provider.forward(options))

    const response = []
    const results = await Promise.allSettled(all)
    results.forEach((result) => {
      if (result.status !== 'fulfilled') {
        app.logger.error(result.reason.toString())
        return
      }

      result.value.forEach((entry) => {
        const normalized = entry.feature
        normalized.geokoder = {
          source: entry.source,
          match: entry.match,
          matchProp: entry.matchProp,
          score: scoreResult(q.toUpperCase(), entry.match.toUpperCase())
        }
        response.push(normalized)
      })
    })

    // sort by score
    response.sort((a, b) => {
      return a.geokoder.score < b.geokoder.score
        ? 1
        : a.geokoder.score > b.geokoder.score
          ? -1
          : 0
    })

    res.json(response)
  })

  app.get('/reverse', async (req, res, next) => {
    const lat = _.toNumber(_.get(req.query, 'lat'))
    const lon = _.toNumber(_.get(req.query, 'lon'))

    if (_.isFinite(lat) && _.isFinite(lon)) {
      const options = { lat, lon, filter: _.get(req.query, 'sources', '*') }
      // Some providers might support additional parameters
      if (_.has(req.query, 'distance')) options.distance = _.toNumber(_.get(req.query, 'distance'))
      if (_.has(req.query, 'limit')) options.limit = _.toInteger(_.get(req.query, 'limit'))
      else if (_.has(paginate, 'default.reverse')) options.limit = _.get(paginate, 'default.reverse')
      if (_.has(paginate, 'max.reverse') && options.limit) options.limit = Math.min(options.limit, _.get(paginate, 'max.reverse'))
      
      const all = Providers.get().filter(provider => typeof provider.reverse === 'function').map(provider => provider.reverse(options))

      const response = []
      const results = await Promise.allSettled(all)
      results.forEach((result) => {
        if (result.status !== 'fulfilled') {
          app.logger.error(result.reason.toString())
          return
        }

        result.value.forEach((entry) => {
          const normalized = entry.feature
          normalized.geokoder = {
            source: entry.source
            // TODO: score by distance ?
            // score: scoreResult(q.toUpperCase(), entry.match.toUpperCase())
          }
          response.push(normalized)
        })
      })

      res.json(response)
    } else {
      throw new errors.BadRequest('Reverse geocoding expect decimal numbers for longitude and latitude')
    }
  })
}