diff --git a/app.py b/app.py index 52b9a7a..6d126a2 100644 --- a/app.py +++ b/app.py @@ -212,6 +212,9 @@ def geocode_batch_route(): coords = body.get("coords") if not isinstance(coords, list) or not coords: return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400 + # Deduplicate — same (lat4,lon4) would hit lru_cache anyway but avoids + # submitting redundant work. + seen = set() result = {} for item in coords: try: @@ -221,9 +224,9 @@ def geocode_batch_route(): continue lat4 = int(round(lat, 4) * 10000) lon4 = int(round(lon, 4) * 10000) - label = _geocode_cached(lat4, lon4) - if label: - result[f"{lat:.3f},{lon:.3f}"] = label + if (lat4, lon4) not in seen: + seen.add((lat4, lon4)) + result[f"{lat:.3f},{lon:.3f}"] = _geocode_cached(lat4, lon4) return jsonify(result) @app.route('/api/dash/devices') diff --git a/server/app.py b/server/app.py index 332d7dc..ba3d9c9 100644 --- a/server/app.py +++ b/server/app.py @@ -2,6 +2,7 @@ uploaded by the Pi, storing them in Postgres + on-disk file storage. Also serves the remote web portal for browsing and managing the configured car. """ +import concurrent.futures import functools import hashlib import os @@ -626,12 +627,20 @@ def geocode(): @app.post("/api/geocode/batch") @require_login def geocode_batch(): - """Batch geocode — returns {key: label} for all unique coords in one request.""" + """Batch geocode — returns {key: label} for all unique coords in one request. + + Parallelised with ThreadPoolExecutor (max_workers=4) so Nominatim HTTP + calls and reverse_geocoder lookups run concurrently instead of sequentially. + _geocode_cached's lru_cache absorbs duplicate coords within the batch.""" body = request.get_json(silent=True) or {} coords = body.get("coords") if not isinstance(coords, list) or not coords: return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400 - result = {} + + # Deduplicate — same (lat4,lon4) would hit lru_cache anyway, but this + # avoids submitting redundant futures. + seen = set() + items = [] for item in coords: try: lat = float(item["lat"]) @@ -640,9 +649,25 @@ def geocode_batch(): continue lat4 = int(round(lat, 4) * 10000) lon4 = int(round(lon, 4) * 10000) - key = f"{lat:.3f},{lon:.3f}" - name = _geocode_cached(lat4, lon4) - result[key] = name + if (lat4, lon4) not in seen: + seen.add((lat4, lon4)) + items.append((f"{lat:.3f},{lon:.3f}", lat4, lon4)) + + result = {} + if not items: + return jsonify(result) + + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex: + futures = {ex.submit(_geocode_cached, lat4, lon4): key for key, lat4, lon4 in items} + for future in concurrent.futures.as_completed(futures): + key = futures[future] + try: + result[key] = future.result(timeout=8) + except Exception: + # Last resort — raw coords if even the fallback failed + parts = key.split(",") + result[key] = f"{float(parts[0]):.5f}, {float(parts[1]):.5f}" + return jsonify(result) diff --git a/static/js/history.js b/static/js/history.js index bebdc1e..029c083 100644 --- a/static/js/history.js +++ b/static/js/history.js @@ -75,31 +75,32 @@ function preload(pts) { if (_geoCtrl) _geoCtrl.abort(); _geoCtrl = new AbortController(); var sig = _geoCtrl.signal; _cache = {}; - var seen = {}, q = [], i; + var seen = {}, coords = [], i; for (i = 0; i < pts.length; i++) { var k = gk(pts[i].latitude, pts[i].longitude); if (!k || seen[k]) continue; seen[k] = 1; - q.push({k: k, lat: pts[i].latitude, lon: pts[i].longitude}); + coords.push({lat: pts[i].latitude, lon: pts[i].longitude}); } - if (!q.length) return; - var C = 2, idx = 0; - function nx() { - if (idx >= q.length || sig.aborted) return; - var it = q[idx++], c = new AbortController(); - var t = setTimeout(function () { c.abort(); }, 3000); - sig.addEventListener("abort", function () { c.abort(); }); - fetch("/api/geocode?lat=" + it.lat + "&lon=" + it.lon, {signal: c.signal}) - .then(function (r) { clearTimeout(t); return r.ok ? r.json() : null; }) - .then(function (d) { if (!sig.aborted) _cache[it.k] = (d && d.label) || null; }) - .catch(function () { clearTimeout(t); }) - .finally(function () { if (!sig.aborted) nx(); }); - } - for (var j = 0; j < C && j < q.length; j++) nx(); + if (!coords.length) return; + // Single batch POST instead of N individual GETs — one roundtrip. + fetch("/api/geocode/batch", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({coords: coords}), + signal: sig + }) + .then(function (r) { return r.ok ? r.json() : {}; }) + .then(function (data) { + if (sig.aborted) return; + for (var key in data) { if (data.hasOwnProperty(key)) _cache[key] = data[key]; } + }) + .catch(function () {}); } function showLoc(lat, lon) { var k = gk(lat, lon); if (!k || k === _lastKey) return; _lastKey = k; var fb = lat.toFixed(5) + ", " + lon.toFixed(5); if (_cache.hasOwnProperty(k)) { locEl.textContent = _cache[k] || fb; return; } + // Fallback: individual fetch only if preload didn't cover this coord. var c = new AbortController(); var t = setTimeout(function () { c.abort(); }, 3000); fetch("/api/geocode?lat=" + lat + "&lon=" + lon, {signal: c.signal}) @@ -112,7 +113,7 @@ } function schedLoc(lat, lon) { clearTimeout(_geoTimer); - _geoTimer = setTimeout(function () { showLoc(lat, lon); }, 600); + _geoTimer = setTimeout(function () { showLoc(lat, lon); }, 200); } // ── Format ───────────────────────────────────────────────────────────