diff --git a/webapp/coordinates.py b/webapp/coordinates.py new file mode 100644 index 0000000..31d37c0 --- /dev/null +++ b/webapp/coordinates.py @@ -0,0 +1,35 @@ + +import math + +from ipe_fetcher import AreaCoordinates +def check_coordinates_area(coordinates: AreaCoordinates, max_area) -> AreaCoordinates: + 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 diff --git a/webapp/eligibility_api/elig_api_routes.py b/webapp/eligibility_api/elig_api_routes.py index 0520ea7..c64dbc2 100644 --- a/webapp/eligibility_api/elig_api_routes.py +++ b/webapp/eligibility_api/elig_api_routes.py @@ -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'") \ No newline at end of file diff --git a/webapp/ipe_fetcher/arcep.py b/webapp/ipe_fetcher/arcep.py index d0ed04d..6af98be 100644 --- a/webapp/ipe_fetcher/arcep.py +++ b/webapp/ipe_fetcher/arcep.py @@ -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( diff --git a/webapp/ipe_fetcher/axione.py b/webapp/ipe_fetcher/axione.py index 7c94b33..e7a059d 100644 --- a/webapp/ipe_fetcher/axione.py +++ b/webapp/ipe_fetcher/axione.py @@ -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,12 +108,14 @@ 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') + date_commandable = date_formatted.strftime('%d/%m/%Y') except ValueError as err: print("Error while mainpulating DateDebutAcceptationCmdAcces from Axione DB: ", err) diff --git a/webapp/ipe_fetcher/liazo.py b/webapp/ipe_fetcher/liazo.py index 79f6cca..db8ee98 100644 --- a/webapp/ipe_fetcher/liazo.py +++ b/webapp/ipe_fetcher/liazo.py @@ -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") - c.request("GET", req) - r = c.getresponse() + 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 @@ -27,7 +36,7 @@ class Liazo: for building in v: fdnEligStatus = FAIEligibilityStatus( isEligible=True, - ftthStatus="DEPLOYE", # Pas de status donc on dit que c'est ok mais on check avec l'arcep si axione KO cote front + ftthStatus="DEPLOYE", # Pas de status donc on dit que c'est ok mais on check avec l'arcep si axione KO cote front reasonNotEligible=None, ) idImm=building.get('ref') @@ -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=""), diff --git a/webapp/main.py b/webapp/main.py index 90c4f71..d1f7611 100644 --- a/webapp/main.py +++ b/webapp/main.py @@ -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]) - except ValueError: - valid_args = False + try: + 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 diff --git a/webapp/static/style.css b/webapp/static/style.css index 43ce804..05f7054 100644 --- a/webapp/static/style.css +++ b/webapp/static/style.css @@ -15,8 +15,8 @@ body { } #btn-load-elig-data { - top: 4em; - right: 0; + top: 10em; + left: 1em; position: fixed; z-index: 1; padding: .5em; @@ -37,4 +37,24 @@ body { .nonDeployee { 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); + } } \ No newline at end of file diff --git a/webapp/templates/app.js b/webapp/templates/app.js index 568ddbc..51296e5 100644 --- a/webapp/templates/app.js +++ b/webapp/templates/app.js @@ -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: '© OpenStreetMap 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,84 +205,84 @@ function initAddrSearch(map) { } function updateEligData(map, eligData) { - if (markers) { - markers.map(marker => map.removeLayer(marker)); - } let buildings = eligData.buildings; - markers = buildings.forEach(building => { - const latlng = new L.latLng(building.y, building.x); - let addrImm = `${building.numVoieImm} ${building.typeVoieImm} ${building.nomVoieImm}` - if (building.bat_info != "") { - addrImm += ` (Bat ${building.bat_info})` - } - let colorMarker = 'black' - let messageElig = `` - eligTestApi = `eligtest/ftth?idImm=${building.idImm}&codePostal=${building.codePostal}&axione=${building.aquilenetEligStatus.isEligible}&liazo=${building.fdnEligStatus.isEligible}` - // éligible chez Aquilenet, lien pour le test - if (building.aquilenetEligStatus.isEligible) { + 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 != "") { + addrImm += ` (Bat ${building.bat_info})` + } + let colorMarker = 'black' + let messageElig = `` + eligTestApi = `eligtest/ftth?idImm=${building.idImm}&codePostal=${building.codePostal}&axione=${building.aquilenetEligStatus.isEligible}&liazo=${building.fdnEligStatus.isEligible}` + // éligible chez Aquilenet, lien pour le test + if (building.aquilenetEligStatus.isEligible) { - // Si fibre Axione déployé mais pas encore commandable - if (building.aquilenetEligStatus.ftthStatus == "DEPLOYE MAIS NON COMMANDABLE") { - colorMarker = 'orange' - messageElig = `

Fibre deployée mais ne sera commandable qu\'à partir du ${building.aquilenetEligStatus.dateCommandable}

` - } else { - messageElig = `

Fibre deployée et disponible par Aquilenet !

` - const zip = encodeURIComponent(building.codePostal); - const idImm = encodeURIComponent(building.idImm); - messageElig += `
Tester l'éligibilité` - colorMarker = 'green' - } + // Si fibre Axione déployé mais pas encore commandable + if (building.aquilenetEligStatus.ftthStatus == "DEPLOYE MAIS NON COMMANDABLE") { + colorMarker = 'orange' + messageElig = `

Fibre deployée mais ne sera commandable qu\'à partir du ${building.aquilenetEligStatus.dateCommandable}

` + } else { + messageElig = `

Fibre deployée et disponible par Aquilenet !

` + const zip = encodeURIComponent(building.codePostal); + const idImm = encodeURIComponent(building.idImm); + messageElig += `
Tester l'éligibilité` + colorMarker = 'green' + } - // pas de données Axione mais Kosc nous renvoie qque chose à cette adresse (fdnEligStatus) - // c'est peut être OK, on croise avec les données ARCEP (othersEligStatus) - // Enfin on affiche un lien vers le test d'éligibilté KOSC à cette adresse - } else if (building.fdnEligStatus.isEligible && building.othersEligStatus.isEligible) { - messageElig = `

Fibre deployee mais pas chez Axione !` - messageElig += `
Tester l'eligibilite par Kosc et Bouygues

` - colorMarker = 'orange' - // Pas de données Kosc ou Axione mais l'ARCEP nous dit qu'une fibre est déployée à cette adresse - } else if (building.othersEligStatus.isEligible) { - messageElig = `

Fibre deployee mais non eligible Aquilenet, desole :(

` - colorMarker = 'red' - // Pas de fibre il semblerait, proposer un test ADSL Aquilenet - } else { - messageElig = `

Fibre non deployee :(

` - const zip = encodeURIComponent(building.codePostal); - const comm = encodeURIComponent(building.commune); - let convertType = streetTypeConversion.get(building.typeVoieImm.toLowerCase()); - if (!convertType) { - convertType = building.typeVoieImm; + // pas de données Axione mais Kosc nous renvoie qque chose à cette adresse (fdnEligStatus) + // c'est peut être OK, on croise avec les données ARCEP (othersEligStatus) + // Enfin on affiche un lien vers le test d'éligibilté KOSC à cette adresse + } else if (building.fdnEligStatus.isEligible && building.othersEligStatus.isEligible) { + messageElig = `

Fibre deployee mais pas chez Axione !` + messageElig += `
Tester l'eligibilite par Kosc et Bouygues

` + colorMarker = 'orange' + // Pas de données Kosc ou Axione mais l'ARCEP nous dit qu'une fibre est déployée à cette adresse + } else if (building.othersEligStatus.isEligible) { + messageElig = `

Fibre deployee mais non eligible Aquilenet, desole :(

` + colorMarker = 'red' + // Pas de fibre il semblerait, proposer un test ADSL Aquilenet + } else { + messageElig = `

Fibre non deployee :(

` + const zip = encodeURIComponent(building.codePostal); + const comm = encodeURIComponent(building.commune); + let convertType = streetTypeConversion.get(building.typeVoieImm.toLowerCase()); + if (!convertType) { + convertType = building.typeVoieImm; + } + const street = encodeURIComponent(`${convertType} ${building.nomVoieImm}`) + const street_nb = encodeURIComponent(building.numVoieImm) + messageElig += `
Tester ADSL a cette adresse` + if (building.othersEligStatus.reasonNotEligible != "") { + messageElig += `

Status general ARCEP: ${building.othersEligStatus.reasonNotEligible}` + } } - const street = encodeURIComponent(`${convertType} ${building.nomVoieImm}`) - const street_nb = encodeURIComponent(building.numVoieImm) - messageElig += `
Tester ADSL a cette adresse` - if (building.othersEligStatus.reasonNotEligible != "") { - messageElig += `

Status general ARCEP: ${building.othersEligStatus.reasonNotEligible}` + // Si pas d'éligibilité fibre, on affiche la raison si elle existe + if (building.aquilenetEligStatus.reasonNotEligible != "") { + messageElig += `
Pour Aquilenet, raison non eligible: ${building.aquilenetEligStatus.reasonNotEligible}` } + var markerIcon = new L.Icon({ + iconUrl: `static/icons/marker-icon-${colorMarker}.png`, + shadowUrl: 'static/vendor/images/marker-shadow.png', + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41] + }); + const marker = new L.marker(latlng, { + icon: markerIcon, + zIndexOffset: - building.etat_imm_priority + }) + .bindPopup(`${addrImm}
${building.codePostal} ${building.commune}` + + `

${messageElig}

Ref Immeuble: ${building.idImm}`, { + maxWidth: 560 + }); + map.addLayer(marker); + markers.set(building.idImm, marker) } - // Si pas d'éligibilité fibre, on affiche la raison si elle existe - if (building.aquilenetEligStatus.reasonNotEligible != "") { - messageElig += `
Pour Aquilenet, raison non eligible: ${building.aquilenetEligStatus.reasonNotEligible}` - } - var markerIcon = new L.Icon({ - iconUrl: `static/icons/marker-icon-${colorMarker}.png`, - shadowUrl: 'static/vendor/images/marker-shadow.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], - shadowSize: [41, 41] - }); - const marker = new L.marker(latlng, { - icon: markerIcon - }) - .bindPopup(`${addrImm}
${building.codePostal} ${building.commune}` + - `

${messageElig}

Ref Immeuble: ${building.idImm}`, { - maxWidth: 560 - }); - map.addLayer(marker); - return 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(); - updateEligData(map, eligData); + 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); \ No newline at end of file +const addrSearch = initAddrSearch(map); +setBtnListener(btn, map); + +// Init a limits box that shows area where data will be fetched +initLimitsBox(map, btn); diff --git a/webapp/templates/map.html b/webapp/templates/map.html index 61116a2..4282d3b 100644 --- a/webapp/templates/map.html +++ b/webapp/templates/map.html @@ -18,14 +18,13 @@ +
-