Webapp: add real elig data query

When a acceptable zoom level is reached, the frontend is going to
query the DB for the eligibility data associated to the region the
user is viewing.

When requesting too much data, the frontend gets pretty slow. In order
to prevent that, we require a minimal zoom level before triggering the
actual request.
This commit is contained in:
Félix Baylac-Jacqué 2022-02-22 12:48:50 +01:00
parent 925d93fc58
commit 38c67ee588
4 changed files with 108 additions and 23 deletions

2
webapp/config.ini.sample Normal file
View file

@ -0,0 +1,2 @@
[DB]
path = /path/to/ipe.sqlite

View file

@ -1,19 +1,77 @@
from flask import Flask, request, render_template from flask import Flask, request, render_template
app = Flask(__name__) from typing import TypedDict
import configparser
import sqlite3
import os
@app.route("/", methods=['GET']) class Config(TypedDict):
dbPath: str
def parseConfig() -> Config:
cfg_path = os.environ.get("CONFIG", "/etc/ipe-ftth-elig/conf.ini")
cfg = configparser.ConfigParser()
with open(cfg_path, "r") as f:
cfg.read_file(f)
return {'dbPath':cfg.get("DB","path")}
app = Flask(__name__)
cfg:Config = parseConfig()
@app.route("/", methods=["GET"])
def getMap(): def getMap():
return render_template("map.html") return render_template("map.html")
@app.route("/eligdata", methods=['GET']) @app.route("/eligdata", methods=["GET"])
def getEligData(): def getEligData():
args = request.args args = request.args
if 'swx' in args and 'swy' in args and \ valid_args = True
'nex' in args and 'ney' in args: processed_args = {}
return { "buildings": [ for k in ['swx', 'swy', 'nex', 'ney']:
{"x": args['swx'], "y":args['swy'], "label": "dummy elig val" }, valid_args = valid_args and k in args
{"x": args['nex'], "y":args['ney'], "label": "dummy elig val 2" } if valid_args:
]} try:
processed_args[k] = float(args[k])
except ValueError:
valid_args = False
if valid_args:
cur = cursorWithSpatialite()
# Let's first see how big is the area we're about to query.
# If it's too big, abort the request to prevent a server DOS.
cur.execute('''
SELECT Area(BuildMBR(:swx,:swy,:nex,:ney,4326))
''',processed_args)
req_area = cur.fetchone()[0]
if req_area <= 0.08:
cur.execute('''
SELECT
X(ImmeubleGeoPoint),
Y(ImmeubleGeoPoint),
IdentifiantImmeuble,
EtatImmeuble,
NumeroVoieImmeuble,
TypeVoieImmeuble,
NomVoieImmeuble
FROM ipe
WHERE ROWID IN (
SELECT ROWID FROM SpatialIndex
WHERE f_table_name = 'ipe' AND
search_frame = BuildMBR(:swx, :swy, :nex, :ney, 4326))
''',processed_args)
buildings = [ {
'x':b[0], 'y':b[1], 'idImm':b[2],
'etatImm':b[3], 'numVoieImm': b[4],
'typeVoieImm': b[5], 'nomVoieImm': b[6]
} for b in cur.fetchall()]
return { "buildings": buildings}
else:
return "The requested area is too wide, please reduce it", 400
else: else:
return "Missing coordinates", 400 return "Invalid bounding box coordinates", 400
def cursorWithSpatialite():
db = sqlite3.connect(cfg['dbPath'])
cur = db.cursor()
db.enable_load_extension(True)
cur.execute('SELECT load_extension("mod_spatialite")')
return cur

View file

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
TEMPLATES_AUTO_RELOAD=true FLASK_ENV=development FLASK_APP=main poetry run flask run --reload CONFIG=./config.ini TEMPLATES_AUTO_RELOAD=true FLASK_ENV=development FLASK_APP=main poetry run flask run --reload

View file

@ -1,9 +1,23 @@
const minZoomForRequest = 17;
let markers = []; let markers = [];
function initMap() { function initMap(btn) {
let map = L.map('map').setView([46.710, 3.669], 6); let map = L.map('map').setView([46.710, 3.669], 6);
L.tileLayer('https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map); }).addTo(map);
map.on("zoom", () => {
console.log(map.getZoom());
/* We only want to enable the search button when we reached a sufficient zoom level */
if (btn.disabled && map.getZoom() >= minZoomForRequest) {
btn.disabled = false;
btn.title = "Rechercher les données d'éligibilité pour cette zone."
}
if (!btn.disabled && map.getZoom() < minZoomForRequest) {
btn.disabled = true;
btn.title = "Veuillez zoomer plus la carte avant de lancer une recherche d'éligibilité.";
}
});
return map; return map;
} }
@ -46,24 +60,35 @@ function updateEligData(map, eligData) {
let buildings = eligData.buildings; let buildings = eligData.buildings;
markers = buildings.map(building => { markers = buildings.map(building => {
const latlng = new L.latLng(building.y, building.x); const latlng = new L.latLng(building.y, building.x);
const marker = new L.marker(latlng).bindPopup(building.label); const addrImm = `${building.numVoieImm} ${building.typeVoieImm} ${building.nomVoieImm}`
const marker = new L.marker(latlng)
.bindPopup(`${addrImm}<br/>Etat: ${building.etatImm}<br/>Code Immeuble: ${building.idImm}`);
map.addLayer(marker); map.addLayer(marker);
return marker return marker
}); });
} }
async function fetchEligData(map) { async function fetchEligData(map) {
const bounds = map.getBounds();
const sw = bounds.getSouthWest();
const ne = bounds.getNorthEast();
const zoom = map.getZoom(); const zoom = map.getZoom();
const reqUri = encodeURI(`/eligdata?swx=${sw.lng}&swy=${sw.lat}&nex=${ne.lng}&ney=${ne.lat}`); if (zoom >= minZoomForRequest) {
const source = await fetch(reqUri); const bounds = map.getBounds();
const eligData = await source.json(); const sw = bounds.getSouthWest();
updateEligData(map, eligData); const ne = bounds.getNorthEast();
const reqUri = encodeURI(`/eligdata?swx=${sw.lng}&swy=${sw.lat}&nex=${ne.lng}&ney=${ne.lat}`);
const source = await fetch(reqUri);
const eligData = await source.json();
updateEligData(map, eligData);
}
} }
const map = initMap(); function initBtn() {
const addrSearch = initAddrSearch(map); const btn = document.getElementById("btn-load-elig-data");
btn.disabled = true;
btn.title = "Veuillez zoomer plus la carte avant de lancer une recherche d'éligibilité.";
btn.onclick = () => fetchEligData(map);
return btn;
}
document.getElementById("btn-load-elig-data").onclick = () => fetchEligData(map); const btn = initBtn();
const map = initMap(btn);
const addrSearch = initAddrSearch(map);