diff --git a/src/c3nav/editor/api/endpoints.py b/src/c3nav/editor/api/endpoints.py index 44f8bb6b..1691464a 100644 --- a/src/c3nav/editor/api/endpoints.py +++ b/src/c3nav/editor/api/endpoints.py @@ -1,16 +1,19 @@ from django.urls import Resolver404, resolve from django.utils.translation import gettext_lazy as _ from ninja import Router as APIRouter +from shapely.geometry.geo import mapping from c3nav.api.auth import APIKeyAuth, auth_permission_responses from c3nav.api.exceptions import API404 from c3nav.editor.api.base import api_etag_with_update_cache_key from c3nav.editor.api.geometries import get_level_geometries_result, get_space_geometries_result -from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, GeometryStylesSchema, UpdateCacheKey +from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, GeometryStylesSchema, UpdateCacheKey, \ + EditorBeaconsLookup from c3nav.editor.views.base import editor_etag_func from c3nav.mapdata.api.base import api_etag from c3nav.mapdata.models import Source from c3nav.mapdata.schemas.responses import WithBoundsSchema +from c3nav.mesh.utils import get_nodes_and_ranging_beacons editor_api_router = APIRouter(tags=["editor"], auth=APIKeyAuth(permissions={"editor_access"})) @@ -145,3 +148,54 @@ def post_view_as_api(request, path: str): this is a mess. good luck. if you actually want to use this, poke us so we might add better documentation. """ raise NotImplementedError + + +@editor_api_router.get('/beacons-lookup/', summary="get beacon coordinates", + description="get xyz coordinates for all known positioning beacons", + response={200: EditorBeaconsLookup, **auth_permission_responses}, + openapi_extra={"security": [{"APIKeyAuth": ["editor_access", "write"]}]}) +def beacons_lookup(request): + # todo: update with more details? todo permission? + from c3nav.mesh.messages import MeshMessageType + calculated = get_nodes_and_ranging_beacons() + + wifi_beacons = {} + ibeacons = {} + for beacon in calculated.beacons.values(): + node = calculated.nodes_for_beacons.get(beacon.id, None) + beacon_data = { + "name": node.name if node else ("Beacon #%d" % beacon.pk), + "point": mapping(beacon.geometry), + } + if beacon.wifi_bssid: + wifi_beacons[beacon.wifi_bssid] = beacon_data + if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None: + ibeacons.setdefault( + str(beacon.ibeacon_uuid), {} + ).setdefault( + beacon.ibeacon_major, {} + )[beacon.ibeacon_minor] = beacon_data + + for node in calculated.nodes.values(): + beacon_data = { + "name": node.name, + "point": None, + } + ibeacon_msg = node.last_messages[MeshMessageType.CONFIG_IBEACON] + if ibeacon_msg: + ibeacons.setdefault( + str(ibeacon_msg.parsed.content.uuid), {} + ).setdefault( + ibeacon_msg.parsed.content.major, {} + ).setdefault( + ibeacon_msg.parsed.content.minor, beacon_data + ) + + node_msg = node.last_messages[MeshMessageType.CONFIG_NODE] + if node_msg: + wifi_beacons.setdefault(node.address, beacon_data) + + return EditorBeaconsLookup( + wifi_beacons=wifi_beacons, + ibeacons=ibeacons, + ).model_dump(mode="json") \ No newline at end of file diff --git a/src/c3nav/editor/api/schemas.py b/src/c3nav/editor/api/schemas.py index 3bfc805d..f8cf25ee 100644 --- a/src/c3nav/editor/api/schemas.py +++ b/src/c3nav/editor/api/schemas.py @@ -1,9 +1,13 @@ from typing import Annotated, Literal, Optional, Union +from uuid import UUID +from annotated_types import Lt from pydantic import Field as APIField from pydantic import PositiveInt +from pydantic.types import NonNegativeInt +from pydantic_extra_types.mac_address import MacAddress -from c3nav.api.schema import AnyGeometrySchema, BaseSchema, GeometrySchema, LineSchema +from c3nav.api.schema import AnyGeometrySchema, BaseSchema, GeometrySchema, LineSchema, PointSchema from c3nav.api.utils import NonEmptyStr GeometryStylesSchema = Annotated[ @@ -100,3 +104,22 @@ UpdateCacheKey = Annotated[ Optional[NonEmptyStr], APIField(default=None, title="the cache key under which you have cached objects"), ] + + +class EditorBeacon(BaseSchema): + name: NonEmptyStr + point: Optional[PointSchema] + + +class EditorBeaconsLookup(BaseSchema): + wifi_beacons: dict[Annotated[MacAddress, APIField(title="WiFi beacon BSSID")], EditorBeacon] + ibeacons: dict[ + Annotated[str, APIField(title="iBeacon UUID")], # todo: nice to use UUID but django json encoder fails + dict[ + Annotated[NonNegativeInt, Lt(2 ** 16), APIField(title="iBeacon major value")], + dict[ + Annotated[NonNegativeInt, Lt(2 ** 16), APIField(title="iBeacon minor value")], + EditorBeacon + ] + ] + ] \ No newline at end of file diff --git a/src/c3nav/editor/static/editor/js/editor.js b/src/c3nav/editor/static/editor/js/editor.js index 23a4c68e..e59edb48 100644 --- a/src/c3nav/editor/static/editor/js/editor.js +++ b/src/c3nav/editor/static/editor/js/editor.js @@ -284,6 +284,8 @@ editor = { editor._inform_mobile_client(content.find('[data-user-data]')); + editor._beacon_layer.clearLayers(); + var group; if (content.find('[name=fixed_x]').length) { $('[name=name]').change(editor._source_name_selected).change(); @@ -414,12 +416,17 @@ editor = { if (data_field.length) { data_field.hide(); var collector = $($('body .scancollector')[0].outerHTML); + editor.load_scancollector_lookup(); + var existing_data = []; if (data_field.val()) { existing_data = JSON.parse(data_field.val()); } - if (existing_data.length > 0) { - collector.removeClass('empty').addClass('done').find('.wifi-count').text(existing_data.length); + if (existing_data?.wifi?.length || existing_data?.ibeacon?.length > 0) { + // todo: fix this to work with ibeacons + collector.removeClass('empty').addClass('done'); + collector.find('.wifi-count').text(existing_data?.wifi?.length); + collector.find('.ibeacon-count').text(existing_data?.ibeacon?.length); } else { data_field.closest('form').addClass('scan-lock'); } @@ -657,6 +664,7 @@ editor = { _geometries_layer: null, _line_geometries: [], _highlight_layer: null, + _beacon_layer: null, _highlight_type: null, _editing_id: null, _editing_layer: null, @@ -775,6 +783,8 @@ editor = { }) editor.get_sources(); + + editor._beacon_layer = L.layerGroup().addTo(editor.map); }, _set_max_bounds: function(bounds) { bounds = bounds ? L.latLngBounds(editor._max_bounds[0], editor._max_bounds[1]).extend(bounds) : editor._max_bounds; @@ -1313,6 +1323,13 @@ editor = { .on('click', '.scancollector .reset', editor._scancollector_reset); window.setInterval(editor._scancollector_wifi_scan_perhaps, 1000); }, + _scancollector_lookup: {}, + load_scancollector_lookup: function () { + c3nav_api.get('editor/beacons-lookup') + .then(data => { + editor._scancollector_lookup = data; + }) + }, _scancollector_data: { wifi: [], ibeacon: [], @@ -1341,16 +1358,17 @@ editor = { }, _scancollector_reset: function () { var $collector = $('#sidebar').find('.scancollector'); - $collector.removeClass('done').removeClass('running').addClass('empty').find('table tbody').each(function(elem) {elem.html('');}); + $collector.removeClass('done').removeClass('running').addClass('empty').find('table tbody').each(function(elem) {elem.innerHTML = "";}); $collector.siblings('[name=data]').val(''); $collector.closest('form').addClass('scan-lock'); + editor._beacon_layer.clearLayers(); }, _scancollector_wifi_last_max_last: 0, _scancollector_wifi_last_result: 0, _scancollector_wifi_result: function(data) { var $collector = $('#sidebar').find('.scancollector.running'), $table = $collector.find('.wifi-table tbody'), - item, i, line, apid, color, max_last = 0, now = Date.now(); + item, i, line, apid, color, max_last = 0, now = Date.now(), match; editor._wifi_scan_waits = false; if (!data.length) return; @@ -1389,9 +1407,22 @@ editor = { if (line.length) { line.removeClass('old').find(':last-child').text(item.rssi).css('color', color); } else { + match = editor._scancollector_lookup.wifi_beacons?.[item.bssid]; + if (match && match.point) { + L.geoJson(match.point, { + pointToLayer: function (feature, latlng) { + return L.circleMarker(latlng, {}); + } + }).addTo(editor._beacon_layer); + } + shortened_ssid = item.ssid; + if (shortened_ssid.length > 20) { + shortened_ssid = shortened_ssid.slice(0, 20)+'…'; + } line = $('
0 WiFi scans
BSSID | SSID | RSSI | |
---|---|---|---|
BSSID | SSID | Match | RSSI |
0 iBeacon scans
Major | Minor | Dist | |
---|---|---|---|
Major | Minor | Match | Dist |