johan/add-api #7
9 changed files with 304 additions and 133 deletions
35
webapp/coordinates.py
Normal file
35
webapp/coordinates.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
|
||||
import math
|
||||
|
||||
from ipe_fetcher import AreaCoordinates
|
||||
def check_coordinates_area(coordinates: AreaCoordinates, max_area) -> AreaCoordinates:
|
||||
johan.le.baut marked this conversation as resolved
|
||||
swx = coordinates.get('swx')
|
||||
swy = coordinates.get('swy')
|
||||
nex = coordinates.get('nex')
|
||||
ney = coordinates.get('ney')
|
||||
x_interval = abs(nex - swx)
|
||||
y_interval = abs(ney - swy)
|
||||
area = x_interval * y_interval
|
||||
|
||||
if area <= max_area:
|
||||
# We are within max area, use original coordinates
|
||||
return coordinates
|
||||
else:
|
||||
# Decrease area size to max area while keeping x y ratio
|
||||
new_x_interval = x_interval * math.sqrt(max_area / (x_interval * y_interval))
|
||||
new_y_interval = y_interval * math.sqrt(max_area / (x_interval * y_interval))
|
||||
return AreaCoordinates(
|
||||
swx=(swx + nex - new_x_interval) / 2,
|
||||
swy=(swy + ney - new_y_interval) / 2,
|
||||
nex=(swx + nex + new_x_interval) / 2,
|
||||
ney=(swy + ney + new_y_interval) / 2,
|
||||
)
|
||||
|
||||
|
||||
def check_coordinates_args(args):
|
||||
processed_args = {}
|
||||
for k in ["swx", "swy", "nex", "ney"]:
|
||||
if k not in args:
|
||||
raise ValueError(f"{k} not in args")
|
||||
processed_args[k] = float(args[k])
|
||||
return processed_args
|
|
@ -1,9 +1,8 @@
|
|||
from flask import Flask, request
|
||||
|
||||
from ipe_fetcher import Axione
|
||||
|
||||
from eligibility_api.elig_api_exceptions import ApiParamException
|
||||
|
||||
from coordinates import check_coordinates_args, check_coordinates_area
|
||||
from ipe_fetcher.axione import AXIONE_MAX_AREA, Axione
|
||||
|
||||
class EligibilityApiRoutes:
|
||||
def __init__(self, flask_app: Flask, axione_ipe: Axione):
|
||||
|
@ -19,3 +18,13 @@ class EligibilityApiRoutes:
|
|||
raise ApiParamException("You need to specify path parameter 'refimmeuble'")
|
||||
return self.axione_ipe.get_eligibilite_per_id_immeuble(refimmeuble)
|
||||
|
||||
|
||||
@self.flask_app.route("/eligibilite/axione/coord", methods=["GET"])
|
||||
def get_axione_eligibility_per_coordinates():
|
||||
args = request.args
|
||||
try:
|
||||
processed_args = check_coordinates_args(args)
|
||||
coordinates = check_coordinates_area(processed_args, AXIONE_MAX_AREA)
|
||||
return self.axione_ipe.getAreaBuildings(coordinates, {})
|
||||
except ValueError:
|
||||
raise ApiParamException("You need to specify path parameters 'swx' 'swy' 'nex' 'ney'")
|
|
@ -16,13 +16,13 @@ class Arcep:
|
|||
@staticmethod
|
||||
def _get_etat_priority(etat_imm):
|
||||
if etat_imm == ARCEP_ETAT_DEPLOYE:
|
||||
return 0
|
||||
return 10
|
||||
elif etat_imm == "en cours de deploiement":
|
||||
return 1
|
||||
return 11
|
||||
elif etat_imm != "abandonne":
|
||||
return 3
|
||||
return 30
|
||||
else:
|
||||
return 4
|
||||
return 31
|
||||
|
||||
|
||||
def getAreaBuildings(
|
||||
|
|
|
@ -15,6 +15,9 @@ AXIONE_ETAT_RACCORDABLE_DEMANDE = "RACCORDABLE DEMANDE"
|
|||
|
||||
AXIONE_REFIMM_TABLE_NAME = "refimm"
|
||||
|
||||
AXIONE_MAX_AREA = 0.08
|
||||
|
||||
|
||||
class Axione:
|
||||
def __init__(self, db_axione_ipe_path: str, db_name: str):
|
||||
self.db_axione_ipe_path = db_axione_ipe_path
|
||||
|
@ -44,9 +47,9 @@ class Axione:
|
|||
elif etat_imm == AXIONE_ETAT_RAD_DEPLOIEMENT:
|
||||
return 3
|
||||
elif etat_imm != AXIONE_ETAT_ABANDONNE:
|
||||
return 4
|
||||
return 20
|
||||
else:
|
||||
return 5
|
||||
return 21
|
||||
|
||||
|
||||
def getAreaBuildings(
|
||||
|
@ -68,7 +71,7 @@ class Axione:
|
|||
areaCoordinates,
|
||||
)
|
||||
req_area = cur.fetchone()[0]
|
||||
if req_area <= 0.08:
|
||||
if req_area <= AXIONE_MAX_AREA:
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT
|
||||
|
@ -105,9 +108,11 @@ class Axione:
|
|||
date_commandable = ""
|
||||
# C'est bien déployé, cependant ce n'est pas encore commandable (donc bientôt et on a la date)
|
||||
# On laisse isEligible = True, côté JS il faut regarder le statut pour laj l'affichage en conséquence
|
||||
|
||||
if isEligible and date_debut:
|
||||
try:
|
||||
date_formatted = datetime.strptime(date_debut, '%Y%m%d').date()
|
||||
|
||||
if date_formatted >= datetime.now().date():
|
||||
etatImm = AXIONE_ETAT_DEPLOYE_NON_COMMANDABLE
|
||||
date_commandable = date_formatted.strftime('%d/%m/%Y')
|
||||
|
|
|
@ -1,20 +1,29 @@
|
|||
import http.client as httplib
|
||||
from ipe_fetcher.model import AreaCoordinates, Building, FAIEligibilityStatus
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
|
||||
class Liazo:
|
||||
def __init__(self):
|
||||
self.https_conn = httplib.HTTPSConnection("vador.fdn.fr")
|
||||
pass
|
||||
|
||||
def getAreaBuildings(
|
||||
self, narrow_coordinates: AreaCoordinates(), existing_buildings: dict
|
||||
) -> dict:
|
||||
nc=narrow_coordinates
|
||||
c = self.https_conn
|
||||
req = "/souscription/gps-batiments.cgi?etape=gps_batiments&lat1=%f&lat2=%f&lon1=%f&lon2=%f" % (nc['swy'],nc['ney'],nc['swx'],nc['nex'])
|
||||
c = httplib.HTTPSConnection("vador.fdn.fr")
|
||||
api_params = "etape=gps_batiments&lat1=%f&lat2=%f&lon1=%f&lon2=%f" % (nc['swy'], nc['ney'], nc['swx'], nc['nex'])
|
||||
req = f"/souscription/gps-batiments.cgi?{api_params}"
|
||||
req = req.replace(" ", "%20")
|
||||
r = None
|
||||
try:
|
||||
c.request("GET", req)
|
||||
r = c.getresponse()
|
||||
except Exception:
|
||||
print(f"Could not call Liazo API to get Buildings, params: {api_params}")
|
||||
print(traceback.format_exc())
|
||||
return existing_buildings
|
||||
if r.status < 200 or r.status >= 300:
|
||||
print("Erreur de serveur chez FDN.")
|
||||
return existing_buildings
|
||||
|
@ -49,7 +58,7 @@ class Liazo:
|
|||
commune="",
|
||||
bat_info="",
|
||||
found_in = ["liazo"],
|
||||
etat_imm_priority=1,
|
||||
etat_imm_priority=4,
|
||||
fdnEligStatus=fdnEligStatus,
|
||||
aquilenetEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
|
||||
othersEligStatus=FAIEligibilityStatus(isEligible=False, reasonNotEligible="", ftthStatus=""),
|
||||
|
|
|
@ -7,7 +7,11 @@ from flask import Flask, request, render_template, redirect
|
|||
from eligibility_api.elig_api_exceptions import FlaskExceptions
|
||||
from eligibility_api.elig_api_routes import EligibilityApiRoutes
|
||||
from ipe_fetcher import Liazo, Axione, Arcep, AreaCoordinates
|
||||
from coordinates import check_coordinates_area, check_coordinates_args
|
||||
|
||||
LIAZO_MAX_X_INTERVAL = 0.0022
|
||||
LIAZO_MAX_Y_INTERVAL = 0.0011
|
||||
LIAZO_MAX_AREA = LIAZO_MAX_X_INTERVAL * LIAZO_MAX_Y_INTERVAL
|
||||
|
||||
class Config(TypedDict):
|
||||
axione_ipe_path: str
|
||||
|
@ -50,43 +54,38 @@ def getEligData():
|
|||
args = request.args
|
||||
valid_args = True
|
||||
processed_args = {}
|
||||
for k in ["swx", "swy", "nex", "ney"]:
|
||||
valid_args = valid_args and k in args
|
||||
if valid_args:
|
||||
try:
|
||||
processed_args[k] = float(args[k])
|
||||
processed_args = check_coordinates_args(args)
|
||||
except ValueError:
|
||||
valid_args = False
|
||||
|
||||
if valid_args:
|
||||
# Need to narrow coordinates for Liazo API call
|
||||
coordinates = check_coordinates_area(processed_args, LIAZO_MAX_AREA)
|
||||
|
||||
# computes center
|
||||
centerx = (processed_args['swx'] + processed_args['nex']) / 2
|
||||
centery = (processed_args['swy'] + processed_args['ney']) / 2
|
||||
|
||||
narrow_x = 0.0022
|
||||
narrow_y = 0.0011
|
||||
|
||||
narrow_coordinates = AreaCoordinates(
|
||||
swx=centerx - narrow_x,
|
||||
swy=centery - narrow_y,
|
||||
nex=centerx + narrow_x,
|
||||
ney=centery + narrow_y,
|
||||
)
|
||||
buildings = dict()
|
||||
try:
|
||||
buildings = arcep.getAreaBuildings(narrow_coordinates, buildings)
|
||||
buildings = axione.getAreaBuildings(narrow_coordinates, buildings)
|
||||
buildings = arcep.getAreaBuildings(coordinates, buildings)
|
||||
buildings = axione.getAreaBuildings(coordinates, buildings)
|
||||
except ValueError as err:
|
||||
print("Could not get Axione data for this area:", err)
|
||||
|
||||
buildings = liazo.getAreaBuildings(narrow_coordinates, buildings)
|
||||
sorted_buildings = sorted(buildings.values(), key=lambda d: d.get('etat_imm_priority', 1), reverse=True)
|
||||
buildings = liazo.getAreaBuildings(coordinates, buildings)
|
||||
|
||||
return {"buildings": list(sorted_buildings)}
|
||||
return {"buildings": list(buildings.values())}
|
||||
else:
|
||||
return "Invalid bounding box coordinates", 400
|
||||
|
||||
|
||||
@app.route("/eligdata/bounds", methods=["GET"])
|
||||
def getEligDataBounds():
|
||||
args = request.args
|
||||
try:
|
||||
processed_args = check_coordinates_args(args)
|
||||
return {"bounds": check_coordinates_area(processed_args, LIAZO_MAX_AREA)}
|
||||
except ValueError:
|
||||
return "Invalid bounding box coordinates", 400
|
||||
|
||||
|
||||
@app.route("/eligtest/ftth", methods=["GET"])
|
||||
def testFtth():
|
||||
args = request.args
|
||||
|
|
|
@ -15,8 +15,8 @@ body {
|
|||
}
|
||||
|
||||
#btn-load-elig-data {
|
||||
top: 4em;
|
||||
right: 0;
|
||||
top: 10em;
|
||||
left: 1em;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
padding: .5em;
|
||||
|
@ -38,3 +38,23 @@ body {
|
|||
display: inline;
|
||||
color: brown;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 5px solid #1787c2;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
const minZoomForRequest = 17;
|
||||
const minZoomForRequest = 16;
|
||||
const urlADSL = 'https://tools.aquilenet.fr/cgi-bin/recherchend.cgi'
|
||||
const urlTestFTTH = 'https://tools.aquilenet.fr/cgi-bin/test.cgi'
|
||||
|
||||
|
@ -71,7 +71,30 @@ streetTypeConversion.set("zone d'aménagement différé", "zad")
|
|||
streetTypeConversion.set("zone industrielle", "zi")
|
||||
streetTypeConversion.set("zone", "zone")
|
||||
|
||||
let markers = [];
|
||||
let markers = new Map();
|
||||
|
||||
// Default search bounds
|
||||
DEFAULT_MAX_LNG_INTERVAL = 0.0028
|
||||
DEFAULT_MAX_LAT_INTERVAL = 0.0014
|
||||
// Search bounds from server
|
||||
server_max_lng_interval = undefined
|
||||
server_max_lat_interval = undefined
|
||||
|
||||
function getRectangleCoord(map) {
|
||||
max_lng_interval = DEFAULT_MAX_LNG_INTERVAL
|
||||
max_lat_interval = DEFAULT_MAX_LAT_INTERVAL
|
||||
if (server_max_lat_interval !== undefined && server_max_lng_interval !== undefined) {
|
||||
max_lng_interval = server_max_lng_interval
|
||||
max_lat_interval = server_max_lat_interval
|
||||
}
|
||||
|
||||
let center = map.getCenter();
|
||||
let corner1 = L.latLng(center.lat - (max_lat_interval / 2), center.lng - (max_lng_interval / 2));
|
||||
let corner2 = L.latLng(center.lat + (max_lat_interval / 2), center.lng + (max_lng_interval / 2));
|
||||
|
||||
return [corner1, corner2]
|
||||
|
||||
}
|
||||
|
||||
function initMap(btn) {
|
||||
// Init map position/zoom. Potentially using what's in the URL search string.
|
||||
|
@ -91,7 +114,8 @@ function initMap(btn) {
|
|||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
map.on("zoom", () => {
|
||||
map.on("zoom move", () => {
|
||||
|
||||
/* We only want to enable the search button when we reached a sufficient zoom level */
|
||||
if (btn.disabled && map.getZoom() >= minZoomForRequest) {
|
||||
displayBtn(btn);
|
||||
|
@ -100,9 +124,52 @@ function initMap(btn) {
|
|||
hideBtn(btn);
|
||||
}
|
||||
});
|
||||
map.on("zoomend moveend", () => {
|
||||
if (map.getZoom() >= minZoomForRequest) {
|
||||
fetchEligData(map);
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
async function initLimitsBox(map, btn) {
|
||||
|
||||
// Create box to show where data is fetched
|
||||
const box = createRectangleBox(map);
|
||||
await getServerBoxBounds(map, box);
|
||||
box.addTo(map);
|
||||
|
||||
map.on("zoom move", () => {
|
||||
box.setBounds(getRectangleCoord(map))
|
||||
})
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
getServerBoxBounds(map, box)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function createRectangleBox(map) {
|
||||
return L.rectangle(getRectangleCoord(map), {color: "#ff7800", fillOpacity: 0.07, weight: 1});
|
||||
}
|
||||
|
||||
// Ask server the narrowed area bounds that it will search in
|
||||
async function getServerBoxBounds(map, box) {
|
||||
const bounds = map.getBounds();
|
||||
const sw = bounds.getSouthWest();
|
||||
const ne = bounds.getNorthEast();
|
||||
const reqUri = encodeURI(`eligdata/bounds?swx=${sw.lng}&swy=${sw.lat}&nex=${ne.lng}&ney=${ne.lat}`);
|
||||
const resp = await fetch(reqUri);
|
||||
if (resp.status != 200) {
|
||||
return
|
||||
}
|
||||
const data = await resp.json();
|
||||
server_max_lat_interval = data.bounds.ney - data.bounds.swy
|
||||
server_max_lng_interval = data.bounds.nex - data.bounds.swx
|
||||
box.setBounds(getRectangleCoord(map))
|
||||
}
|
||||
|
||||
function initAddrSearch(map) {
|
||||
const autocompleteOptions = {
|
||||
debounceTime: 300,
|
||||
|
@ -138,11 +205,9 @@ function initAddrSearch(map) {
|
|||
}
|
||||
|
||||
function updateEligData(map, eligData) {
|
||||
if (markers) {
|
||||
markers.map(marker => map.removeLayer(marker));
|
||||
}
|
||||
let buildings = eligData.buildings;
|
||||
markers = buildings.forEach(building => {
|
||||
buildings.forEach(building => {
|
||||
if (! markers.has(building.idImm)) {
|
||||
const latlng = new L.latLng(building.y, building.x);
|
||||
let addrImm = `${building.numVoieImm} ${building.typeVoieImm} ${building.nomVoieImm}`
|
||||
if (building.bat_info != "") {
|
||||
|
@ -208,14 +273,16 @@ function updateEligData(map, eligData) {
|
|||
shadowSize: [41, 41]
|
||||
});
|
||||
const marker = new L.marker(latlng, {
|
||||
icon: markerIcon
|
||||
icon: markerIcon,
|
||||
zIndexOffset: - building.etat_imm_priority
|
||||
})
|
||||
.bindPopup(`${addrImm}<br/>${building.codePostal} ${building.commune}` +
|
||||
`<br/><br/>${messageElig}<br/><br/>Ref Immeuble: ${building.idImm}`, {
|
||||
maxWidth: 560
|
||||
});
|
||||
map.addLayer(marker);
|
||||
return marker
|
||||
markers.set(building.idImm, marker)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -230,12 +297,17 @@ async function fetchEligData(map) {
|
|||
const bounds = map.getBounds();
|
||||
const sw = bounds.getSouthWest();
|
||||
const ne = bounds.getNorthEast();
|
||||
const btn = document.getElementById("btn-load-elig-data");
|
||||
let btn = document.getElementById("btn-load-elig-data");
|
||||
waitBtn(btn);
|
||||
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();
|
||||
const resp = await fetch(reqUri);
|
||||
if (resp.status == 200) {
|
||||
const eligData = await resp.json();
|
||||
updateEligData(map, eligData);
|
||||
} else {
|
||||
error = await resp.text()
|
||||
console.log(`Error could not get data from server: ${resp.status} ${error}`)
|
||||
}
|
||||
updateUrl(map);
|
||||
displayBtn(btn);
|
||||
}
|
||||
|
@ -244,27 +316,50 @@ async function fetchEligData(map) {
|
|||
function initBtn() {
|
||||
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);
|
||||
btn.title = "Veuillez zoomer plus la carte pour charger l'éligibilité.";
|
||||
return btn;
|
||||
}
|
||||
|
||||
function setBtnListener(btn, map) {
|
||||
btn.onclick = () => {
|
||||
// Reset markers when button is clicked
|
||||
if (markers) {
|
||||
|
||||
for (let marker of markers.values()){
|
||||
map.removeLayer(marker);
|
||||
}
|
||||
markers.clear();
|
||||
}
|
||||
fetchEligData(map);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function displayBtn(btn) {
|
||||
btn.classList.remove('loader');
|
||||
btn.disabled = false;
|
||||
btn.title = "Rechercher les données d'éligibilité pour cette zone."
|
||||
btn.title = "Actualiser la recherche dans cette zone"
|
||||
btn.innerHTML = "Actualiser";
|
||||
}
|
||||
|
||||
function hideBtn(btn) {
|
||||
btn.disabled = true;
|
||||
btn.title = "Veuillez zoomer plus la carte avant de lancer une recherche d'éligibilité.";
|
||||
btn.innerHTML = "Zoomez sur la carte";
|
||||
btn.title = "Veuillez zoomer plus la carte afin de lancer la recherche d'éligibilité.";
|
||||
}
|
||||
|
||||
function waitBtn(btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = "";
|
||||
btn.title = "Chargement des batiments...";
|
||||
btn.classList.add('loader');
|
||||
}
|
||||
|
||||
|
||||
// Init button and map
|
||||
const btn = initBtn();
|
||||
const map = initMap(btn);
|
||||
const addrSearch = initAddrSearch(map);
|
||||
setBtnListener(btn, map);
|
||||
|
||||
// Init a limits box that shows area where data will be fetched
|
||||
initLimitsBox(map, btn);
|
||||
|
|
|
@ -18,14 +18,13 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<button id="btn-load-elig-data" type="button" disabled>Zoomez sur la carte</button>
|
||||
<div class="autocomplete" id="search-addr-autocomplete">
|
||||
<input id="search-addr-autocomplete-input" class="autocomplete-input"
|
||||
spellcheck="false" autocorrect="off"t e autocomplete="off"
|
||||
autocapitalize="off" placeholder="Votre Adresse"/>
|
||||
<ul class="autocomplete-result-list"/>
|
||||
</div>
|
||||
<button id="btn-load-elig-data" type="button">Récupérer les données d'éligibilité
|
||||
pour cette zone</button>
|
||||
<div id="map"/>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
|
|
Loading…
Reference in a new issue
We changed the function behaviour. Let's align the name with the new function.
Something like: