Commit fb0c882f authored by Adrien Pavie's avatar Adrien Pavie
Browse files

Boundaries + doc through API, common config file for API + DB

parent f5ba1d1b
/api/doc
/api/node_modules
/api/*.log
......@@ -7,21 +7,78 @@ Osm4odf (OpenStreetMap pour OpenDataFrance) est un outil simple d'extraction th
* Et récupérer les données issues d'OpenStreetMap sur ce thème et sur ce territoire en CSV ou GeoJSON
## Serveur
## Dépendances
### Base de données
* NodeJS >= 9
* PostgreSQL >= 9.6 et ses extensions PostGIS et HStore
* wget
* [osm2pgsql](https://github.com/openstreetmap/osm2pgsql)
* [jq](https://stedolan.github.io/jq/)
Vous devez disposer d'une base de données PostgreSQL dans une version récente (>= 9.6) avec les extensions suivantes installées :
* `hstore`
* `postgis`
## Configuration
### Configuration du script
Les paramètres généraux de configuration du service sont à renseigner dans le ficchier `config.json`. Les variables à renseigner sont les suivantes :
Les configurations principales (accès à la base de données, dossier de travail, fichier PBF à récupérer...) sont à renseigner dans le fichier `db/run.sh`.
* `WORK_DIR` : dossier de travail, dans lequel les fichiers temporaires sont stockés (exemple: `/tmp/osm4odf`)
* `PBF_URL` : URL de téléchargement de l'extrait OpenStreetMap au format PBF (exemple: `http://download.geofabrik.de/europe/france-latest.osm.pbf`)
* `PG_HOST` : nom d'hôte pour accéder à la base de données PostgreSQL (exemple: `localhost`)
* `PG_USER` : nom d'utilisateur pour accéder à la base de données PostgreSQL (exemple: `postgres`)
* `PG_USER` : numéro de port pour accéder à la base de données PostgreSQL (exemple: `5432`)
* `PG_DB_CLEAN` : nom de la base de données qui contiendra les données propres (exemple: `osm4odf`)
* `PG_DB_TMP` : nom de la base de données qui contiendra les données en cours d'import (exemple: `osm4odf_next`)
* `PG_DB_OLD` : nom de la base de données qui contiendra les données précédemment en production (exemple: `osm4odf_prev`)
* `PARALLEL_JOBS` : nombre de tâches à exécuter en parallèle, idéalement 75% des coeurs du processeur (exemple: `6`)
* `CACHE_MEM` : mémoire à allouer lors de l'import de données, en MB, idéalement 75% de la mémoire vive disponible (exemple: `10000`)
## Base de données
La base de données contient les informations issues d'OpenStreetMap, afin de les mettre à disposition de l'API.
### Script d'import
La configuration de la structure des données en bases (tags sous forme de colonnes, type d'objets sous forme d'emprise...) sont à détailler dans le fichier `db/import.style`. Un exemple complet de ce type de fichier [est disponible ici](https://github.com/openstreetmap/osm2pgsql/blob/master/default.style). Il est recommandé de ne faire apparaître sous forme de colonne dédiée uniquement les tags qui feront partie des filtres des requêtes formulées par l'API.
Une fois les configurations réalisées, exécutez simplement les commandes suivantes :
```bash
cd db/
./run.sh
```
## API
L'API propose au site web client (ou à des connexions directes) de télécharger les données thématiques. Elle interroge la base de données PostgreSQL.
### Installation des dépendances
Pour s'exécuter, l'API nécessite que NodeJS (>= 9) soit installé sur la machine. Pour installer les dépendances, lancez les commandes suivantes :
```bash
cd api/
npm install
```
### Exécution
Pour lancer l'API, il suffit d'exécuter la commande suivante :
```bash
npm run start
# Alternativement, vous pouvez changer le numéro de port avec cette commande
PORT=12345 npm run start
```
### Utilisation
Une fois l'API lancée, vous avez accès aux adresses suivantes :
* [localhost:3000](http://localhost:3000/) : interface utilisateur de requêtage
* [localhost:3000/doc](http://localhost:3000/doc) : documentation de l'API
## Licence
......
This diff is collapsed.
{
"name": "osm4odf-api",
"version": "0.0.0",
"description": "API to extract ready-to-use OpenStreetMap data on various themes",
"main": "src/index.js",
"scripts": {
"start": "npm run clean:log && npm run doc && node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"doc": "apidoc -i src/ -o doc/",
"clean": "npm run clean:log && rm -rf ./doc",
"clean:log": "rm api.log"
},
"author": "Adrien Pavie",
"license": "AGPL-3.0",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"pg": "^7.14.0"
},
"devDependencies": {
"apidoc": "^0.18.0"
},
"apidoc": {
"title": "OpenStreetMap for OpenDataFrance"
}
}
/**
* This file manages communication with PostgreSQL database
*/
const { Pool } = require('pg');
const fs = require('fs');
const CONFIG = require('../../config.json');
// Create pool of connections
const pool = new Pool({
user: CONFIG.PG_USER,
host: CONFIG.PG_HOST,
database: CONFIG.PG_DB_CLEAN,
port: CONFIG.PG_PORT
});
/**
* Checks if database is ready to receive requests (running and not being updated)
*/
function whenAvailable(t) {
t = t || 0;
if(t === 10) { return Promise.reject(); }
return new Promise((resolve, reject) => {
if(fs.existsSync(CONFIG.WORK_DIR+"/db.lock")) {
setTimeout(() => {
resolve(whenAvailable(t+1));
}, 1000);
}
else {
pool.query("SELECT 1+1")
.then(() => resolve())
.catch(() => {
setTimeout(() => {
resolve(whenAvailable(t+1));
}, 1000);
});
}
});
}
/**
* Retrieve list of available boundaries
*/
function getBoundaries() {
return whenAvailable()
.then(() => {
return new Promise((resolve, reject) => {
fs.readFile(CONFIG.WORK_DIR+"/bounds.json", (err, data) => {
if(err) { return reject(err); }
return resolve({ categories: JSON.parse(data) });
});
});
});
}
module.exports = {
getBoundaries: getBoundaries
};
/**
* API main code
*/
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const db = require('./db');
// Init API
const app = express();
const port = process.env.PORT || 3000;
app.use(cors());
app.options('*', cors());
// Log queries
app.use((req, res, next) => {
const toLog = {
ip: req.ip,
path: req.path,
query: req.query,
ts: Date.now()
};
fs.appendFile('api.log', JSON.stringify(toLog)+'\n', (err) => {
if(err) {
console.log("Can't write into log file", err);
}
});
next();
});
/*
* List of routes
*/
/**
* @api {get} /doc API documentation
* @apiDescription Get the user documentation of the API (this page)
* @apiName GetDoc
* @apiGroup Default
*/
app.use('/doc', express.static(__dirname+'/../doc'));
/**
* @api {get} /data/:themeId/:boundaryId Thematic data extract
* @apiDescription Get OpenStreetMap data for given theme, in given administrative boundary
* @apiName GetData
* @apiGroup Default
*
* @apiParam {String} themeId Theme of data to extract (one in /themes list)
* @apiParam {String} boundaryId Boundary of area where data must be extracted (one in /boundaries list)
* @apiParam {String} [format=csv] Output file format (csv, geojson, shapefile)
* @apiParam {Number} [radius] Filter by radius around boundary centroid (in kilometers) instead of boundary polygon
*
* @apiExample As GeoJSON:
* http://localhost:3000/data/bicycle_parking/1234?format=geojson
* @apiExample As CSV, using radius:
* http://localhost:3000/data/bicycle_parking/1234?format=csv&radius=5
*/
/**
* @api {get} /themes List of themes
* @apiDescription Get all data themes available
* @apiName GetThemes
* @apiGroup Default
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "themes": [
* {
* "id": "bicycle_parking",
* "name:en": "Bicycle parkings",
* "name:fr": "Parking à vélo"
* },
* ...
* ]
* }
*/
/**
* @api {get} /boundaries List of administrative boundaries
* @apiDescription Get all administrative boundaries that can be used to filter data queries
* @apiName GetBoundaries
* @apiGroup Default
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "categories": [
* {
* "id": "admin_8",
* "name:en": "City",
* "name:fr": "Commune",
* "list": [
* {
* "name": "Rennes",
* "id": "1234"
* },
* ...
* ]
* },
* ...
* ]
* }
*/
app.get("/boundaries", (req, res) => {
db.getBoundaries()
.then(b => res.send(b))
.catch(e => {
console.log(e);
res.status(500).send("Can't read boundaries data");
});
});
// 404
app.use((req, res) => {
res.status(404).send(req.originalUrl + ' not found')
});
// Start
app.listen(port, () => {
console.log('API started on port: ' + port);
});
{
"WORK_DIR": "/tmp/osm4odf",
"PBF_URL": "http://download.geofabrik.de/europe/france/bretagne-latest.osm.pbf",
"PARALLEL_JOBS": "6",
"PG_HOST": "localhost",
"PG_USER": "postgres",
"PG_PORT": "5432",
"PG_DB_CLEAN": "osm4odf",
"PG_DB_TMP": "osm4odf_next",
"PG_DB_OLD": "osm4odf_prev",
"CACHE_MEM": 10000
}
......@@ -3,53 +3,13 @@
##############################################
# #
# Download and import raw OpenStreetMap data #
# Dependencies: #
# - Wget #
# - Psql #
# - Osm2PgSQL #
# #
##############################################
##########################################################################
#
# Configuration
# Please change values according to your server settings
#
# Work directory
# Defines where temporary files should be written on filesystem
WORK_DIR="/tmp/osm4odf"
# OpenStreetMap PBF extract
# You can choose the region you want on http://download.geofabrik.de/
PBF_URL="http://download.geofabrik.de/europe/france/bretagne-latest.osm.pbf" # "http://download.geofabrik.de/europe/france-latest.osm.pbf"
# PostgreSQL connection settings
# PG_DB_CLEAN is the name of final database (production)
# and PG_DB_TMP the name of next production database (to allow import in parallel)
PG_HOST="localhost"
PG_USER="postgres"
PG_PORT="5432"
PG_DB_CLEAN="osm4odf"
PG_DB_TMP="osm4odf_next"
PG_DB_OLD="osm4odf_prev"
# Parallel jobs
# You can set this value as 75% of your amount of CPU cores (and max 8)
PARALLEL_JOBS="6"
# Memory for import cache (in MB)
# You can set this as 75% of your server amount of RAM
CACHE_MEM="10000"
##########################################################################
#
# Script itself
# DO NOT CHANGE BELOW THIS (unless you know what you are doing) !
#
# Read configuration from JSON file
for s in $(cat "../config.json" | jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" ); do
export $s
done
start=`date +%s`
......@@ -102,6 +62,39 @@ osm2pgsql --create \
# Add boundaries for API requests
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -d "${PG_DB_TMP}" -c "CREATE TABLE boundary AS SELECT osm_id, CASE WHEN boundary = 'administrative' THEN 'admin_'||admin_level ELSE boundary END AS type, name, way FROM planet_osm_polygon WHERE boundary IN ('administrative', 'local_authority', 'political') AND (admin_level IS NULL OR (admin_level::int >= 6 AND admin_level::int <= 8)); CREATE INDEX boundary_osm_id_idx ON boundary(osm_id); CREATE INDEX boundary_way_idx ON boundary USING GIST(way);"
# JSON boundary version for API
bounds_json="
WITH types(id, \"name:en\", \"name:fr\")
AS (VALUES
('admin_6', 'Department', 'Département'),
('admin_7', 'Pays', 'Pays (loi Voynet)'),
('admin_8', 'City', 'Commune'),
('political', 'Canton', 'Canton'),
('local_authority', 'Local authority', 'EPCI')
)
SELECT
array_to_json(array_agg(json_build_object(
'id', type,
'list', blist,
'name:en', \"name:en\",
'name:fr', \"name:fr\"
))) AS data
FROM (
SELECT
type,
t.\"name:en\",
t.\"name:fr\",
array_to_json(array_agg(json_build_object(
'id', osm_id,
'name', name
))) AS blist
FROM boundary
LEFT JOIN types t ON t.id = type
WHERE name IS NOT NULL
GROUP BY type, t.\"name:en\", t.\"name:fr\"
) a;
"
# Alert API that database will be unavailable
echo "Stop API queries to update database"
......@@ -109,13 +102,13 @@ lock_file="${WORK_DIR}/db.lock"
echo "lock" > "${lock_file}"
sleep 5
# Kill still running queries
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${PG_DB_CLEAN}'"
# Change bounds
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -d "${PG_DB_TMP}" -t -A -c "${bounds_json}" > "${WORK_DIR}/bounds.json"
# Rename databases to make next goes live
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -c "DROP DATABASE IF EXISTS ${PG_DB_OLD}"
set +e
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -c "ALTER DATABASE ${PG_DB_CLEAN} RENAME TO ${PG_DB_OLD}"
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${PG_DB_CLEAN}'; ALTER DATABASE ${PG_DB_CLEAN} RENAME TO ${PG_DB_OLD}"
set -e
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -c "ALTER DATABASE ${PG_DB_TMP} RENAME TO ${PG_DB_CLEAN}"
......
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