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

Unified search bar for boundaries

parent 080e2020
......@@ -2462,6 +2462,11 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"fuse.js": {
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.4.6.tgz",
"integrity": "sha512-H6aJY4UpLFwxj1+5nAvufom5b2BT2v45P1MkPvdGIK8fWjQx/7o6tTT1+ALV0yawQvbmvCF0ufl2et8eJ7v7Cg=="
},
"get-assigned-identifiers": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
......@@ -2902,6 +2907,11 @@
"semver": "5.3.0"
}
},
"jquery-slim": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/jquery-slim/-/jquery-slim-3.0.0.tgz",
"integrity": "sha1-h6svoQUBeY6TEkpLXrlTyFN5cH4="
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
......@@ -3613,6 +3623,11 @@
"integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==",
"dev": true
},
"popper.js": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz",
"integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw=="
},
"posix-character-classes": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
......
......@@ -7,7 +7,7 @@
"start": "npm run clean:log && npm run doc && npm run build && node src/index.js",
"start:prod": "npm run clean:log && npm run doc && npm run build && while [ true ]; do node src/index.js; sleep 5; done",
"build": "npm run build:assets && npm run build:js",
"build:assets": "mkdir -p build && cp src/website/*.html node_modules/bootstrap/dist/css/bootstrap.min.css build/",
"build:assets": "mkdir -p build && cp src/website/*.html node_modules/bootstrap/dist/css/bootstrap.min.css node_modules/bootstrap/dist/js/bootstrap.min.js node_modules/jquery-slim/dist/jquery.slim.min.js node_modules/popper.js/dist/umd/popper.min.js build/",
"build:js": "simplifyify --minify --outfile build/index.min.js src/website/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"doc": "apidoc -i src/ -o doc/",
......@@ -21,7 +21,10 @@
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"pg": "^7.14.0"
"fuse.js": "^3.4.6",
"jquery-slim": "^3.0.0",
"pg": "^7.14.0",
"popper.js": "^1.16.0"
},
"devDependencies": {
"apidoc": "^0.18.0",
......
......@@ -55,7 +55,7 @@ function getBoundaries() {
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) });
return resolve(JSON.parse(data));
});
});
});
......@@ -131,7 +131,10 @@ ${theme.sql
// Send failure message if any
if(result.stderr.length > 0) {
throw new Error("Can't extract data from DB: "+result.stderr);
// Check if it's not a warning
if(result.stderr.split('\n').filter(l => l.trim().length > 0 && !l.trim().startsWith('Warning')).length > 0) {
throw new Error("Can't extract data from DB: "+result.stderr);
}
}
console.log(`Execution time (${theme.id} on ${boundary}): ${Date.now()-start}ms`);
......
......@@ -167,23 +167,16 @@ app.get("/themes", (req, res) => {
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "categories": [
* {
* "id": "admin_8",
* "name:en": "City",
* "name:fr": "Commune",
* "list": [
* {
* "name": "Rennes",
* "id": "1234"
* },
* ...
* ]
* },
* ...
* ]
* }
* [
* {
* "id": "1234",
* "name": "Rennes",
* "type": "admin_8",
* "type:fr": "Commune",
* "type:en": "City"
* },
* ...
* ]
*/
app.get("/boundaries", (req, res) => {
db.getBoundaries()
......
......@@ -43,15 +43,13 @@
<select id="theme" class="form-control"></select>
</div>
<div class="form-row">
<div class="form-group col-md-5">
<label for="boundary-type">Type de territoire</label>
<select id="boundary-type" class="form-control"></select>
</div>
<div class="form-group col-md-7">
<label for="boundary-search">Nom</label>
<input list="boundaries" id="boundary-search" placeholder="Saisissez le nom du territoire..." class="form-control" />
<datalist id="boundaries"></datalist>
<div class="form-group">
<label for="boundary-search">Territoire</label>
<div class="dropdown">
<input type="text" class="form-control dropdown-toggle" id="boundary-search" data-toggle="dropdown" data-display="static" placeholder="Nom d'une commune, EPCI, canton..." />
<div class="dropdown-menu" id="boundary-suggestions">
<a class="dropdown-item disabled">Tapez un nom pour lancer la recherche</a>
</div>
</div>
</div>
......@@ -96,6 +94,9 @@
<p>Données sous licence ODbL <a href="https://www.openstreetmap.org/copyright">&copy; contributeurs OpenStreetMap</a><br />Site web réalisé par <a href="https://pavie.info">Adrien Pavie</a> pour OpenDataFrance</p>
</div>
<script src="jquery.slim.min.js"></script>
<script src="popper.min.js"></script>
<script src="bootstrap.min.js"></script>
<script src="index.min.js"></script>
</body>
</html>
const Fuse = require("fuse.js");
const data = {};
const url = window.location.href;
const url = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')) + "/";
let selectedBoundary = null;
setDownloadable(false);
......@@ -29,7 +31,7 @@ request(url+"themes")
});
request(url+"boundaries")
.then(boundaries => data.boundaries = JSON.parse(boundaries).categories)
.then(boundaries => data.boundaries = JSON.parse(boundaries))
.catch(e => {
data.boundaries = "error";
console.error(e);
......@@ -55,44 +57,55 @@ function createForm() {
selectTheme.appendChild(tdom);
});
// Show boundary search
const selectBoundaryType = document.getElementById("boundary-type");
data.boundaries.forEach(category => {
const cdom = document.createElement("option");
cdom.value = category.id;
cdom.innerHTML = category["name:fr"];
if(category.id === "admin_8") {
cdom.selected = "true";
initSearchBar(category.id);
}
selectBoundaryType.appendChild(cdom);
});
selectBoundaryType.addEventListener("change", () => {
initSearchBar(selectBoundaryType.value);
// Set-up boundary search
const fuse = new Fuse(data.boundaries, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"name",
"ref",
"type:fr"
]
});
const searchTextField = document.getElementById("boundary-search");
const searchSuggestions = document.getElementById("boundary-suggestions");
searchTextField.value = "";
searchTextField.addEventListener("input", e => {
// Find associated option
const result = [...document.getElementById("boundaries").children].find(c => c.innerHTML.toLowerCase() === searchTextField.value.toLowerCase());
if(result) {
selectedBoundary = result["data-value"];
setDownloadable(true);
$('#boundary-search').dropdown('show');
selectedBoundary = null;
setDownloadable(false);
const text = e.target.value;
if(text && text.trim().length === 0) {
searchSuggestions.innerHTML = '<a class="dropdown-item disabled">Tapez un nom pour lancer la recherche</a>';
}
else {
selectedBoundary = null;
setDownloadable(false);
searchSuggestions.innerHTML = '';
const results = fuse.search(text);
if(results.length > 0) {
results.slice(0, 10).forEach(r => {
const entry = document.createElement("a");
entry.classList.add("dropdown-item");
entry.innerHTML = r.name + ' <small>(' + r["type:fr"] + ' - ' + r.ref + ')</small>';
searchSuggestions.appendChild(entry);
entry.addEventListener("click", () => {
searchTextField.value = r.name + ' (' + r["type:fr"] + ' - ' + r.ref + ')';
selectedBoundary = r.id;
setDownloadable(true);
});
});
}
else {
searchSuggestions.innerHTML = '<a class="dropdown-item disabled">Aucun résultat</a>';
}
}
});
// Enable options toggle button
const btnOpts = document.getElementById("options-toggle");
const divOpts = document.getElementById("options");
btnOpts.addEventListener("click", () => {
divOpts.classList.toggle("show");
});
// Radius selector with special value 0
const lblRadius = document.getElementById("radius-val");
......
{
"WORK_DIR": "/home/adrien/tmp/osm4odf",
"PBF_URL": "http://download.geofabrik.de/europe/france/bretagne-latest.osm.pbf",
"PBF_URL": "http://download.geofabrik.de/europe/france/reunion-latest.osm.pbf",
"PARALLEL_JOBS": "6",
"PG_HOST": "localhost",
......
......@@ -66,11 +66,11 @@ for col in `cat ./import.style | grep -v '^#' | grep -v '^$' | sed -r 's/ +/#/g'
done
# 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, ST_SimplifyPreserveTopology(way, 10) AS way, ST_Centroid(way) AS center 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); CREATE INDEX boundary_center_idx ON boundary USING GIST(center);"
psql -h "${PG_HOST}" -U "${PG_USER}" -p "${PG_PORT}" -d "${PG_DB_TMP}" -c "CREATE TABLE boundary AS SELECT osm_id, COALESCE(tags->'ref:INSEE', tags->'ref:NUTS', tags->'ref') AS ref, CASE WHEN boundary = 'administrative' THEN 'admin_'||admin_level ELSE boundary END AS type, name, ST_SimplifyPreserveTopology(way, 10) AS way, ST_Centroid(way) AS center 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); CREATE INDEX boundary_center_idx ON boundary USING GIST(center);"
# JSON boundary version for API
bounds_json="
WITH types(id, \"name:en\", \"name:fr\")
WITH types(id, en, fr)
AS (VALUES
('admin_6', 'Department', 'Département'),
('admin_7', 'Pays', 'Pays (loi Voynet)'),
......@@ -80,25 +80,15 @@ AS (VALUES
)
SELECT
array_to_json(array_agg(json_build_object(
'id', type,
'list', blist,
'name:en', \"name:en\",
'name:fr', \"name:fr\"
'id', b.osm_id,
'name', b.name,
'ref', b.ref,
'type', b.type,
'type:en', t.en,
'type:fr', t.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;
FROM boundary b
LEFT JOIN types t on t.id = b.type;
"
......
Supports Markdown
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