From f7bf30bd7ec897c55f38e57709934609c3674b40 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Fri, 27 Dec 2024 21:33:14 +0100 Subject: [PATCH 01/63] hopefully fix multipoint shape delete error --- src/c3nav/static/leaflet/leaflet.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/c3nav/static/leaflet/leaflet.js b/src/c3nav/static/leaflet/leaflet.js index 4b21ac32..a2fe2f49 100644 --- a/src/c3nav/static/leaflet/leaflet.js +++ b/src/c3nav/static/leaflet/leaflet.js @@ -7870,6 +7870,13 @@ var MultiPoint = Layer.extend({ this._latlngs = this._convertLatLngs(latlngs); }, + + + setLatLngs: function(latlngs) { + this._setLatLngs(latlngs); + }, + + // convert latlngs input into actual LatLng instances; calculate bounds along the way _convertLatLngs: function (latlngs) { var result = []; From da4cc419ca2c8794bc2f873767e5c102b25b3c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 27 Dec 2024 22:05:19 +0100 Subject: [PATCH 02/63] introduce ap_name and implement bssid_from_scans_to_beacons --- src/c3nav/editor/forms.py | 2 +- .../commands/bssid_from_scans_to_beacons.py | 12 +++++++ .../migrations/0134_rangingbeacon_ap_name.py | 36 +++++++++++++++++++ src/c3nav/mapdata/models/geometry/space.py | 26 +++++++++++--- src/c3nav/mapdata/quests/positioning.py | 6 ++-- 5 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py create mode 100644 src/c3nav/mapdata/migrations/0134_rangingbeacon_ap_name.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 8d2fa519..a7db5b13 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -384,7 +384,7 @@ def create_editor_form(editor_model): 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name', 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'edit_access_restriction', 'default_height', 'door_height', 'outside', 'identifyable', 'can_search', 'can_describe', 'geometry', 'single', 'altitude', - 'level_index', 'short_label', 'origin_space', 'target_space', 'data', 'comment', 'slow_down_factor', + 'level_index', 'short_label', 'origin_space', 'target_space', 'data', "ap_name", 'comment', 'slow_down_factor', 'groundaltitude', 'node_number', 'wifi_bssids', 'bluetooth_address', 'group', 'ibeacon_uuid', 'ibeacon_major', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', 'can_report_mistake', 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', 'enter_description', diff --git a/src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py b/src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py new file mode 100644 index 00000000..e1f9680e --- /dev/null +++ b/src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py @@ -0,0 +1,12 @@ +from django.core.management.base import BaseCommand +from django.db import transaction + +from c3nav.mapdata.models.geometry.space import BeaconMeasurement + + +class Command(BaseCommand): + help = 'collect BSSIDS for AP names from measurements' + + def handle(self, *args, **options): + with transaction.atomic(): + BeaconMeasurement.contribute_bssid_to_beacons(BeaconMeasurement.objects.all()) \ No newline at end of file diff --git a/src/c3nav/mapdata/migrations/0134_rangingbeacon_ap_name.py b/src/c3nav/mapdata/migrations/0134_rangingbeacon_ap_name.py new file mode 100644 index 00000000..07dd6b2f --- /dev/null +++ b/src/c3nav/mapdata/migrations/0134_rangingbeacon_ap_name.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0.8 on 2024-12-27 20:46 + +from django.db import migrations, models + + +def fill_ap_name(apps, schema_editor): + RangingBeacon = apps.get_model('mapdata', 'rangingbeacon') + for ranging_beacon in RangingBeacon.objects.filter(import_tag__startswith='noc:'): + ranging_beacon.ap_name = ranging_beacon.import_tag[4:] + if ranging_beacon.comment == ranging_beacon.import_tag[4:]: + ranging_beacon.comment = None + ranging_beacon.save() + + +def unfill_ap_name(apps, schema_editor): + RangingBeacon = apps.get_model('mapdata', 'rangingbeacon') + for ranging_beacon in RangingBeacon.objects.filter(ap_name__isnull=False, import_tag__startswith='noc:'): + if ranging_beacon.ap_name == ranging_beacon.import_tag[4:]: + ranging_beacon.comment = ' '.join(((ranging_beacon.comment or ''), ranging_beacon.ap_name)).strip() + ranging_beacon.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0133_beaconmeasurement_fill_quest'), + ] + + operations = [ + migrations.AddField( + model_name='rangingbeacon', + name='ap_name', + field=models.TextField(blank=True, null=True, verbose_name='AP name'), + ), + migrations.RunPython(fill_ap_name, unfill_ap_name), + ] diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index f38ae720..d833939e 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -444,7 +444,7 @@ class BeaconMeasurement(SpaceGeometryMixin, models.Model): verbose_name=_('author')) comment = models.TextField(null=True, blank=True, verbose_name=_('comment')) data: BeaconMeasurementDataSchema = SchemaField(BeaconMeasurementDataSchema, - verbose_name=_('Measurement list'), + verbose_name=_('Measurement list'), default=BeaconMeasurementDataSchema()) fill_quest = models.BooleanField(_('create a quest to fill this'), default=False) @@ -462,6 +462,22 @@ class BeaconMeasurement(SpaceGeometryMixin, models.Model): def geometry_changed(self): return False + @staticmethod + def contribute_bssid_to_beacons(items: list["BeaconMeasurement"]): + map_name = {} + for item in items: + for scan in item.data.wifi: + for peer in scan: + if peer.ap_name: + map_name.setdefault(peer.ap_name, []).append(peer.bssid) + for beacon in RangingBeacon.objects.filter(ap_name__in=map_name.keys()): + beacon.wifi_bssids = list(set(beacon.wifi_bssids) | set(map_name[beacon.ap_name])) + beacon.save() + + def save(self, *args, **kwargs): + self.contribute_bssid_to_beacons([self]) + return super().save(*args, **kwargs) + class RangingBeacon(SpaceGeometryMixin, models.Model): """ @@ -498,6 +514,7 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): altitude = models.DecimalField(_('altitude above ground'), max_digits=6, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))]) + ap_name = models.TextField(null=True, blank=True, verbose_name=_('AP name')) comment = models.TextField(null=True, blank=True, verbose_name=_('comment')) altitude_quest = models.BooleanField(_('altitude quest'), default=True) @@ -517,10 +534,11 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): @property def title(self): - if self.node_number is not None or self.wifi_bssids: + if self.node_number is not None or self.wifi_bssids or self.ap_name: if self.comment: - return f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])} ({self.comment})'.strip() + return (f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])} {self.ap_name or ''} ' + f' ({self.comment})').strip() else: - return f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])}'.strip() + return f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])} {self.ap_name or ''}'.strip() else: return self.comment diff --git a/src/c3nav/mapdata/quests/positioning.py b/src/c3nav/mapdata/quests/positioning.py index 1ef49903..c1b1a9c1 100644 --- a/src/c3nav/mapdata/quests/positioning.py +++ b/src/c3nav/mapdata/quests/positioning.py @@ -91,8 +91,8 @@ class RangingBeaconBSSIDsQuest(Quest): def quest_description(self) -> list[str]: return [ _("This quest only works in the app. It works fully automatically."), - _("We are trying to find the BSSIDs broadcast by “%s”.") % self.obj.title, - _("Please stand near “%s” and wait for the submit button to appear.") % self.obj.title, + _("We are trying to find the BSSIDs broadcast by “%s”.") % self.obj.ap_name, + _("Please stand near “%s” and wait for the submit button to appear.") % self.obj.ap_name, _("Do not close this popup until then."), _("This should happen within less than a minute."), ] @@ -103,7 +103,7 @@ class RangingBeaconBSSIDsQuest(Quest): @classmethod def _qs_for_request(cls, request): - return RangingBeacon.qs_for_request(request).filter(import_tag__startswith="noc:", wifi_bssids=[]) + return RangingBeacon.qs_for_request(request).filter(ap_name__isnull=False, wifi_bssids=[]) class BeaconMeasurementQuestForm(ChangeSetModelForm): From c5c8df767830f40579ffddc366c137c76c80e6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 27 Dec 2024 22:19:48 +0100 Subject: [PATCH 03/63] moving by 20cm is okay actually --- .../mapdata/management/commands/importnoc.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/c3nav/mapdata/management/commands/importnoc.py b/src/c3nav/mapdata/management/commands/importnoc.py index b1ee8b2f..cc20d3d1 100644 --- a/src/c3nav/mapdata/management/commands/importnoc.py +++ b/src/c3nav/mapdata/management/commands/importnoc.py @@ -39,9 +39,6 @@ class Command(BaseCommand): self.do_import(items) MapUpdate.objects.create(type='importnoc') - def _get_space_geom(self, space): - return space.geometry.difference(unary_union([unwrap_geom(hole.geometry) for hole in space.holes.all()])) - def do_import(self, items: dict[str, NocImportItem]): spaces_for_level = {} levels = tuple(Level.objects.values_list("pk", flat=True)) @@ -86,8 +83,11 @@ class Command(BaseCommand): continue # move point into space if needed - if not new_space.geometry.intersects(new_geometry): - new_geometry = nearest_points(new_space.geometry.buffer(-0.05), new_geometry)[0] + new_space_geometry = new_space.geometry.difference( + unary_union([unwrap_geom(hole.geometry) for hole in new_space.columns.all()]) + ) + if not new_space_geometry.intersects(new_geometry): + new_geometry = nearest_points(new_space_geometry.buffer(-0.05), new_geometry)[0] elif len(possible_spaces) == 1: new_space = possible_spaces[0] print(f"SUCCESS: {name} is in {new_space.title}") @@ -118,17 +118,22 @@ class Command(BaseCommand): old_result = None # build resulting object + altitude_quest = True if not result: old_result = result result = RangingBeacon(import_tag=import_tag) else: if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.03: continue + if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.20: + altitude_quest = False + result.comment = name result.space = new_space result.geometry = new_geometry result.altitude = 0 - result.altitude_quest = True + if altitude_quest: + result.altitude_quest = True result.save() # todo: onyl save if changes… etc for import_tag, location in beacons_so_far.items(): From 279fa1eed5bda05ada68abd0d00fa3a59725e820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 27 Dec 2024 22:20:09 +0100 Subject: [PATCH 04/63] remove todo --- src/c3nav/mapdata/management/commands/importnoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/management/commands/importnoc.py b/src/c3nav/mapdata/management/commands/importnoc.py index cc20d3d1..3ab88af4 100644 --- a/src/c3nav/mapdata/management/commands/importnoc.py +++ b/src/c3nav/mapdata/management/commands/importnoc.py @@ -134,7 +134,7 @@ class Command(BaseCommand): result.altitude = 0 if altitude_quest: result.altitude_quest = True - result.save() # todo: onyl save if changes… etc + result.save() for import_tag, location in beacons_so_far.items(): location.delete() From 0b8b2cad676705b30394885fd2adbc41c44dc84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 27 Dec 2024 22:22:18 +0100 Subject: [PATCH 05/63] remove unneeded code --- src/c3nav/mapdata/management/commands/importnoc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/c3nav/mapdata/management/commands/importnoc.py b/src/c3nav/mapdata/management/commands/importnoc.py index 3ab88af4..b8c752b8 100644 --- a/src/c3nav/mapdata/management/commands/importnoc.py +++ b/src/c3nav/mapdata/management/commands/importnoc.py @@ -115,12 +115,9 @@ class Command(BaseCommand): # find existing location result = beacons_so_far.pop(import_tag, None) - old_result = None - # build resulting object altitude_quest = True if not result: - old_result = result result = RangingBeacon(import_tag=import_tag) else: if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.03: From 88d9051f1ca3791ad951cc5b2aa3312503d409d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 27 Dec 2024 22:59:23 +0100 Subject: [PATCH 06/63] better location? lets see it fail! --- src/c3nav/routing/locator.py | 63 +++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index ef058060..2bf5f3a0 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -24,7 +24,8 @@ try: except ImportError: from threading import local as LocalContext -LocatorPeerIdentifier: TypeAlias = MacAddress | tuple[UUID, Annotated[NonNegativeInt, Lt(2 ** 16)], Annotated[NonNegativeInt, Lt(2 ** 16)]] +LocatorPeerIdentifier: TypeAlias = MacAddress | str | tuple[ + UUID, Annotated[NonNegativeInt, Lt(2 ** 16)], Annotated[NonNegativeInt, Lt(2 ** 16)]] @dataclass @@ -32,6 +33,7 @@ class LocatorPeer: identifier: LocatorPeerIdentifier frequencies: set[int] = field(default_factory=set) xyz: Optional[tuple[int, int, int]] = None + space_id: Optional[int] = None @dataclass @@ -91,6 +93,7 @@ class Locator: int(beacon.geometry.y * 100), int((router.altitude_for_point(beacon.space_id, beacon.geometry) + float(beacon.altitude)) * 100), ) + self.peers[peer_id].space_id = beacon.space_id self.xyz = np.array(tuple(peer.xyz for peer in self.peers)) for space in Space.objects.prefetch_related('beacon_measurements'): @@ -122,8 +125,11 @@ class Locator: for scan_value in scan_data: if settings.WIFI_SSIDS and scan_value.ssid not in settings.WIFI_SSIDS: continue - peer_id = self.get_peer_id(scan_value.bssid, create=create_peers) - if peer_id is not None: + peer_ids = { + self.get_peer_id(scan_value.bssid, create=create_peers), + self.get_peer_id(scan_value.ap_name, create=create_peers), + } - {None, ""} + for peer_id in peer_ids: result[peer_id] = ScanDataValue(rssi=scan_value.rssi, distance=scan_value.distance) return result @@ -201,8 +207,54 @@ class Locator: if result is not None: return result + result = self.locate_by_beacon_positions(scan_data, permissions) + if result is not None: + return result + return self.locate_rssi(scan_data, permissions) + def locate_by_beacon_positions(self, scan_data: ScanData, permissions=None): + scan_data_we_can_use = [ + (peer_id, value) for peer_id, value in scan_data.items() if self.peers[peer_id].space_id + ] + + if not scan_data_we_can_use: + return None + + # get visible spaces + best_ap_id = min(scan_data_we_can_use, key=lambda item: item[1].rssi)[0] + space_id = self.peers[best_ap_id].space_id + space = self.spaces[space_id] + + scan_data_in_the_same_room = sorted([ + (peer_id, value) for peer_id, value in scan_data_we_can_use if self.peers[peer_id].space_id == space_id + ], key=lambda a: a[1].rssi) + + if len(scan_data_in_the_same_room) == 1: + point = space.point + return CustomLocation( + level=space.level_id, + x=point.x, + y=point.y, + permissions=permissions, + icon='my_location' + ) + else: + the_sum = sum(a[1].rssi for a in scan_data_in_the_same_room[:3]) + + x = 0 + y = 0 + for peer_id, value in scan_data_in_the_same_room[:3]: + x = self.peers[peer_id].xyz[0] * value.rssi / the_sum + y = self.peers[peer_id].xyz[1] * value.rssi / the_sum + return CustomLocation( + level=space.level_id, + x=x, + y=y, + permissions=permissions, + icon='my_location' + ) + def locate_rssi(self, scan_data: ScanData, permissions=None): router = Router.load() restrictions = router.get_restrictions(permissions) @@ -227,6 +279,9 @@ class Locator: if best_location is not None: best_location.score = best_score + if best_location is not None: + return None + return best_location @cached_property @@ -312,7 +367,7 @@ class Locator: )], x=result_pos[0]/100, y=result_pos[1]/100, - permissions=(), + permissions=permissions, icon='my_location' ) location.z = result_pos[2]/100 From 10f680442346ec026d341f831492defa1616ac44 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Fri, 27 Dec 2024 23:10:09 +0100 Subject: [PATCH 07/63] cluster map location markers (especially nearby) --- src/c3nav/site/static/site/js/c3nav.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 47199afa..22b12eb6 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1644,7 +1644,14 @@ c3nav = { const level = c3nav.levels[i]; const layerGroup = c3nav._levelControl.addLevel(level[0], level[2]); c3nav._detailLayers[level[0]] = L.layerGroup().addTo(layerGroup); - c3nav._locationLayers[level[0]] = L.layerGroup().addTo(layerGroup); + c3nav._locationLayers[level[0]] = L.markerClusterGroup({ + maxClusterRadius: 35, + spiderLegPolylineOptions: { + color: '#4b6c97', + }, + showCoverageOnHover: false, + iconCreateFunction: makeClusterIconCreate('#4b6c97'), + }).addTo(layerGroup); c3nav._routeLayers[level[0]] = L.layerGroup().addTo(layerGroup); c3nav._userLocationLayers[level[0]] = L.layerGroup().addTo(layerGroup); c3nav._overlayLayers[level[0]] = L.layerGroup().addTo(layerGroup); From 879520a7cd52080dab246b6905c11123ffb42010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 27 Dec 2024 23:28:14 +0100 Subject: [PATCH 08/63] create mapupdate in bssid from scans to beacons --- .../management/commands/bssid_from_scans_to_beacons.py | 5 ++++- src/c3nav/mapdata/models/geometry/space.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py b/src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py index e1f9680e..916033b5 100644 --- a/src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py +++ b/src/c3nav/mapdata/management/commands/bssid_from_scans_to_beacons.py @@ -1,6 +1,7 @@ from django.core.management.base import BaseCommand from django.db import transaction +from c3nav.mapdata.models import MapUpdate from c3nav.mapdata.models.geometry.space import BeaconMeasurement @@ -9,4 +10,6 @@ class Command(BaseCommand): def handle(self, *args, **options): with transaction.atomic(): - BeaconMeasurement.contribute_bssid_to_beacons(BeaconMeasurement.objects.all()) \ No newline at end of file + with MapUpdate.lock(): + BeaconMeasurement.contribute_bssid_to_beacons(BeaconMeasurement.objects.all()) + MapUpdate.objects.create(type='bssids_from_scans_to_beacons') diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index d833939e..c8a6df3a 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -471,6 +471,7 @@ class BeaconMeasurement(SpaceGeometryMixin, models.Model): if peer.ap_name: map_name.setdefault(peer.ap_name, []).append(peer.bssid) for beacon in RangingBeacon.objects.filter(ap_name__in=map_name.keys()): + print(beacon, "add ssids", set(map_name[beacon.ap_name])) beacon.wifi_bssids = list(set(beacon.wifi_bssids) | set(map_name[beacon.ap_name])) beacon.save() From a2b19dad4e69f87367b3fdbe19fdbdd337374c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 00:45:14 +0100 Subject: [PATCH 09/63] purge cache after map updates --- src/c3nav/mapdata/models/update.py | 15 ++++++++++++++- src/c3nav/mapdata/tasks.py | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/models/update.py b/src/c3nav/mapdata/models/update.py index 4212a9b2..7841164b 100644 --- a/src/c3nav/mapdata/models/update.py +++ b/src/c3nav/mapdata/models/update.py @@ -14,7 +14,7 @@ from django.utils.timezone import make_naive from django.utils.translation import gettext_lazy as _ from shapely.ops import unary_union -from c3nav.mapdata.tasks import process_map_updates +from c3nav.mapdata.tasks import process_map_updates, delete_map_cache_key from c3nav.mapdata.utils.cache.changes import GeometryChangeTracker from c3nav.mapdata.utils.cache.local import per_request_cache @@ -193,6 +193,14 @@ class MapUpdate(models.Model): logger = logging.getLogger('c3nav') with cls.get_updates_to_process() as new_updates: + prev_keys = ( + cls.current_processed_cache_key(), + cls.current_processed_geometry_cache_key(), + ) + + for key in prev_keys: + transaction.on_commit(lambda: delete_map_cache_key.delay(cache_key=key)) + if not new_updates: return () @@ -274,6 +282,10 @@ class MapUpdate(models.Model): if self.geometries_changed is None: self.geometries_changed = not changed_geometries.is_empty + old_cache_key = None + if new: + old_cache_key = self.current_cache_key() + super().save(**kwargs) with suppress(FileExistsError): @@ -283,6 +295,7 @@ class MapUpdate(models.Model): pickle.dump(changed_geometries, open(self._changed_geometries_filename(), 'wb')) if new: + transaction.on_commit(lambda: delete_map_cache_key.delay(cache_key=old_cache_key)) transaction.on_commit( lambda: per_request_cache.set('mapdata:last_update', self.to_tuple, None) ) diff --git a/src/c3nav/mapdata/tasks.py b/src/c3nav/mapdata/tasks.py index 4d9c9e6c..8b1d1836 100644 --- a/src/c3nav/mapdata/tasks.py +++ b/src/c3nav/mapdata/tasks.py @@ -47,3 +47,9 @@ def process_map_updates(self): 'date': date_format(updates[-1].datetime, 'DATETIME_FORMAT'), 'id': updates[-1].pk, }) + + +@app.task(bind=True, max_retries=10) +def delete_map_cache_key(self, cache_key): + for key in cache.keys(f'*{cache_key}*'): + cache.delete(key) From ca6252583cdd20e30ec95d3536cf4db3c3bd6469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 14:59:09 +0100 Subject: [PATCH 10/63] beacon_type support --- .../mapdata/management/commands/importpoc.py | 138 ++++++++++++++++++ .../0135_rangingbeacon_beacon_type.py | 24 +++ src/c3nav/mapdata/models/geometry/space.py | 29 +++- src/c3nav/routing/locator.py | 44 ++++-- 4 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 src/c3nav/mapdata/management/commands/importpoc.py create mode 100644 src/c3nav/mapdata/migrations/0135_rangingbeacon_beacon_type.py diff --git a/src/c3nav/mapdata/management/commands/importpoc.py b/src/c3nav/mapdata/management/commands/importpoc.py new file mode 100644 index 00000000..b8c752b8 --- /dev/null +++ b/src/c3nav/mapdata/management/commands/importpoc.py @@ -0,0 +1,138 @@ +import hashlib + +import requests +from django.conf import settings +from django.core.management.base import BaseCommand +from pydantic import BaseModel +from shapely import distance + +from c3nav.mapdata.models import MapUpdate, Space, Level +from c3nav.mapdata.models.geometry.space import RangingBeacon +from c3nav.mapdata.models.report import Report +from c3nav.mapdata.utils.cache.changes import changed_geometries +from c3nav.mapdata.utils.geometry import unwrap_geom +from shapely.ops import nearest_points, unary_union + + +class NocImportItem(BaseModel): + """ + Something imported from the NOC + """ + lat: float | int + lng: float | int + layer: str + type: str = "unknown" + + +class Command(BaseCommand): + help = 'import APs from noc' + + def handle(self, *args, **options): + r = requests.get(settings.NOC_BASE+"/api/markers/get") + r.raise_for_status() + items = {name: NocImportItem.model_validate(item) + for name, item in r.json()["markers"].items() + if not name.startswith("__polyline")} + + with MapUpdate.lock(): + changed_geometries.reset() + self.do_import(items) + MapUpdate.objects.create(type='importnoc') + + def do_import(self, items: dict[str, NocImportItem]): + spaces_for_level = {} + levels = tuple(Level.objects.values_list("pk", flat=True)) + lower_levels_for_level = {pk: levels[:i] for i, pk in enumerate(levels)} + + for space in Space.objects.select_related('level').prefetch_related('holes'): + spaces_for_level.setdefault(space.level_id, []).append(space) + + beacons_so_far: dict[str, RangingBeacon] = { + **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="noc:")}, + } + + for name, item in items.items(): + import_tag = f"noc:{name}" + + if item.type != "AP": + continue + + # determine geometry + converter = settings.NOC_LAYERS.get(item.layer, None) + if not converter: + print(f"ERROR: {name} has invalid layer: {item.layer}") + continue + + new_geometry = converter.convert(item.lat, item.lng) + + # determine space + possible_spaces = [space for space in spaces_for_level[converter.level_id] + if space.geometry.intersects(new_geometry)] + if not possible_spaces: + possible_spaces = [space for space in spaces_for_level[converter.level_id] + if distance(unwrap_geom(space.geometry), new_geometry) < 0.3] + if len(possible_spaces) == 1: + new_space = possible_spaces[0] + the_distance = distance(unwrap_geom(new_space.geometry), new_geometry) + print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}") + elif len(possible_spaces) == 2: + new_space = min(possible_spaces, key=lambda s: distance(unwrap_geom(s.geometry), new_geometry)) + print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}...") + else: + print(f"ERROR: {name} is not within any space (NOC: {(item.lat, item.lng)}, NAV: {new_geometry}") + continue + + # move point into space if needed + new_space_geometry = new_space.geometry.difference( + unary_union([unwrap_geom(hole.geometry) for hole in new_space.columns.all()]) + ) + if not new_space_geometry.intersects(new_geometry): + new_geometry = nearest_points(new_space_geometry.buffer(-0.05), new_geometry)[0] + elif len(possible_spaces) == 1: + new_space = possible_spaces[0] + print(f"SUCCESS: {name} is in {new_space.title}") + else: + print(f"WARNING: {name} could be in multiple spaces, picking one...") + new_space = possible_spaces[0] + + lower_levels = lower_levels_for_level[new_space.level_id] + for lower_level in reversed(lower_levels): + # let's go through the lower levels + if not unary_union([unwrap_geom(h.geometry) for h in new_space.holes.all()]).intersects(new_geometry): + # current selected spacae is fine, that's it + break + print(f"NOTE: {name} is in a hole, looking lower...") + + # find a lower space + possible_spaces = [space for space in spaces_for_level[lower_level] + if space.geometry.intersects(new_geometry)] + if possible_spaces: + new_space = possible_spaces[0] + print(f"NOTE: {name} moved to lower space {new_space}") + else: + print(f"WARNING: {name} couldn't find a lower space, still in a hole") + + # find existing location + result = beacons_so_far.pop(import_tag, None) + + # build resulting object + altitude_quest = True + if not result: + result = RangingBeacon(import_tag=import_tag) + else: + if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.03: + continue + if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.20: + altitude_quest = False + + result.comment = name + result.space = new_space + result.geometry = new_geometry + result.altitude = 0 + if altitude_quest: + result.altitude_quest = True + result.save() + + for import_tag, location in beacons_so_far.items(): + location.delete() + print(f"NOTE: {import_tag} was deleted") diff --git a/src/c3nav/mapdata/migrations/0135_rangingbeacon_beacon_type.py b/src/c3nav/mapdata/migrations/0135_rangingbeacon_beacon_type.py new file mode 100644 index 00000000..95ff6c84 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0135_rangingbeacon_beacon_type.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.8 on 2024-12-28 13:26 + +from django.db import migrations, models + + +def add_beacon_type(apps, schema_editor): + RangingBeacon = apps.get_model('mapdata', 'rangingbeacon') + RangingBeacon.objects.filter(import_tag__startswith='noc:').update(beacon_type="event_wifi") + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0134_rangingbeacon_ap_name'), + ] + + operations = [ + migrations.AddField( + model_name='rangingbeacon', + name='beacon_type', + field=models.CharField(blank=True, choices=[('event_wifi', 'Event WiFi AP'), ('dect', 'DECT antenna')], max_length=16, null=True, verbose_name='beacon type'), + ), + migrations.RunPython(add_beacon_type, migrations.RunPython.noop), + ] diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index c8a6df3a..15a7c812 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -484,8 +484,15 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): """ A ranging beacon """ + class BeaconType(models.TextChoices): + EVENT_WIFI = "event_wifi", _("Event WiFi AP") + DECT = "dect", _("DECT antenna") + geometry = GeometryField('point') + beacon_type = models.CharField(_('beacon type'), choices=BeaconType.choices, + null=True, blank=True, max_length=16) + node_number = models.PositiveSmallIntegerField(_('Node Number'), unique=True, null=True, blank=True) wifi_bssids: list[MacAddress] = SchemaField(list[MacAddress], verbose_name=_('WiFi BSSIDs'), default=list, @@ -535,11 +542,19 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): @property def title(self): - if self.node_number is not None or self.wifi_bssids or self.ap_name: - if self.comment: - return (f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])} {self.ap_name or ''} ' - f' ({self.comment})').strip() - else: - return f'{self.node_number or ''} {''.join(self.wifi_bssids[:1])} {self.ap_name or ''}'.strip() + segments = [] + if self.node_number is not None: + segments.append(self.node_number) + if self.ap_name is not None: + segments.append(f'"{self.ap_name}"') + if segments: + title = ' - '.join(segments).strip() else: - return self.comment + title = f'#{self.pk}' + if self.wifi_bssids: + ssids = self.wifi_bssids[0] + (', …' if len(self.wifi_bssids) > 1 else '') + title += f' ({ssids})' + if self.comment: + title += f' ({self.comment})' + + return f'{self.get_beacon_type_display() if self.beacon_type else self._meta.verbose_name} {title}' diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 2bf5f3a0..a25130e9 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -1,9 +1,9 @@ import operator import pickle from dataclasses import dataclass, field +from enum import StrEnum from functools import cached_property, reduce -from pprint import pprint -from typing import Annotated +from typing import Annotated, NamedTuple, Union from typing import Optional, Self, Sequence, TypeAlias from uuid import UUID @@ -24,13 +24,25 @@ try: except ImportError: from threading import local as LocalContext -LocatorPeerIdentifier: TypeAlias = MacAddress | str | tuple[ - UUID, Annotated[NonNegativeInt, Lt(2 ** 16)], Annotated[NonNegativeInt, Lt(2 ** 16)]] + +class PeerType(StrEnum): + WIFI = "wifi" + DECT = "dect" + IBEACON = "ibeacon" + + +class TypedIdentifier(NamedTuple): + peer_type: PeerType + identifier: Union[ + MacAddress, + str, + tuple[UUID, Annotated[NonNegativeInt, Lt(2 ** 16)], Annotated[NonNegativeInt, Lt(2 ** 16)]] + ] @dataclass class LocatorPeer: - identifier: LocatorPeerIdentifier + identifier: TypedIdentifier frequencies: set[int] = field(default_factory=set) xyz: Optional[tuple[int, int, int]] = None space_id: Optional[int] = None @@ -67,7 +79,7 @@ class LocatorPoint: @dataclass class Locator: peers: list[LocatorPeer] = field(default_factory=list) - peer_lookup: dict[LocatorPeerIdentifier, int] = field(default_factory=dict) + peer_lookup: dict[TypedIdentifier, int] = field(default_factory=dict) xyz: np.array = field(default_factory=(lambda: np.empty((0,)))) spaces: dict[int, "LocatorSpace"] = field(default_factory=dict) @@ -83,9 +95,11 @@ class Locator: for beacon in calculated.beacons.values(): identifiers = [] for bssid in beacon.wifi_bssids: - identifiers.append(bssid) + identifiers.append(TypedIdentifier(PeerType.WIFI, bssid)) if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None: - identifiers.append((beacon.ibeacon_uuid, beacon.ibeacon_major, beacon.ibeacon_minor)) + identifiers.append( + TypedIdentifier(PeerType.IBEACON, (beacon.ibeacon_uuid, beacon.ibeacon_major, beacon.ibeacon_minor)) + ) for identifier in identifiers: peer_id = self.get_peer_id(identifier, create=True) self.peers[peer_id].xyz = ( @@ -111,7 +125,7 @@ class Locator: if new_space.points: self.spaces[space.pk] = new_space - def get_peer_id(self, identifier: LocatorPeerIdentifier, create=False) -> Optional[int]: + def get_peer_id(self, identifier: TypedIdentifier, create=False) -> Optional[int]: peer_id = self.peer_lookup.get(identifier, None) if peer_id is None and create: peer = LocatorPeer(identifier=identifier) @@ -126,9 +140,9 @@ class Locator: if settings.WIFI_SSIDS and scan_value.ssid not in settings.WIFI_SSIDS: continue peer_ids = { - self.get_peer_id(scan_value.bssid, create=create_peers), - self.get_peer_id(scan_value.ap_name, create=create_peers), - } - {None, ""} + self.get_peer_id(TypedIdentifier(PeerType.WIFI, scan_value.bssid), create=create_peers), + self.get_peer_id(TypedIdentifier(PeerType.WIFI, scan_value.ap_name), create=create_peers), + } - {None, ""} for peer_id in peer_ids: result[peer_id] = ScanDataValue(rssi=scan_value.rssi, distance=scan_value.distance) return result @@ -137,7 +151,7 @@ class Locator: result = {} for scan_value in scan_data: peer_id = self.get_peer_id( - (scan_value.uuid, scan_value.major, scan_value.minor), + TypedIdentifier(PeerType.IBEACON, (scan_value.uuid, scan_value.major, scan_value.minor)), create=create_peers ) if peer_id is not None: @@ -185,13 +199,13 @@ class Locator: def convert_raw_scan_data(self, raw_scan_data: list[LocateWifiPeerSchema]) -> ScanData: return self.convert_wifi_scan(raw_scan_data, create_peers=False) - def get_xyz(self, identifier: LocatorPeerIdentifier) -> tuple[int, int, int] | None: + def get_xyz(self, identifier: TypedIdentifier) -> tuple[int, int, int] | None: i = self.get_peer_id(identifier) if i is None: return None return self.peers[i].xyz - def get_all_nodes_xyz(self) -> dict[LocatorPeerIdentifier, tuple[float, float, float]]: + def get_all_nodes_xyz(self) -> dict[TypedIdentifier, tuple[float, float, float]]: return { peer.identifier: peer.xyz for peer in self.peers[:len(self.xyz)] if isinstance(peer.identifier, MacAddress) From cfed6d9ed7d3604c4d6f207e954672d4d903759b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 15:21:53 +0100 Subject: [PATCH 11/63] =?UTF-8?q?wifi=5Fbssids=20=E2=86=92=20addresses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/c3nav/editor/api/endpoints.py | 2 +- src/c3nav/editor/forms.py | 12 +++--- .../management/commands/findbeacons.py | 2 +- .../mapdata/management/commands/importnoc.py | 5 ++- .../0136_wifi_bssids_to_addresses_and_more.py | 38 +++++++++++++++++++ src/c3nav/mapdata/models/geometry/space.py | 15 ++++---- src/c3nav/mapdata/quests/positioning.py | 10 ++--- src/c3nav/mesh/api.py | 2 +- src/c3nav/mesh/models.py | 4 +- src/c3nav/mesh/utils.py | 4 +- src/c3nav/routing/locator.py | 2 +- src/c3nav/site/static/site/js/c3nav.js | 4 +- 12 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0136_wifi_bssids_to_addresses_and_more.py diff --git a/src/c3nav/editor/api/endpoints.py b/src/c3nav/editor/api/endpoints.py index 75c11ca2..e9b95d3d 100644 --- a/src/c3nav/editor/api/endpoints.py +++ b/src/c3nav/editor/api/endpoints.py @@ -170,7 +170,7 @@ def beacons_lookup(request): "name": node.name if node else ("Beacon #%d" % beacon.pk), "point": mapping(beacon.geometry), } - for bssid in beacon.wifi_bssids: + for bssid in beacon.addresses: wifi_beacons[bssid] = beacon_data if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None: ibeacons.setdefault( diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index a7db5b13..1fddd69d 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -384,12 +384,12 @@ def create_editor_form(editor_model): 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name', 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'edit_access_restriction', 'default_height', 'door_height', 'outside', 'identifyable', 'can_search', 'can_describe', 'geometry', 'single', 'altitude', - 'level_index', 'short_label', 'origin_space', 'target_space', 'data', "ap_name", 'comment', 'slow_down_factor', - 'groundaltitude', 'node_number', 'wifi_bssids', 'bluetooth_address', 'group', 'ibeacon_uuid', 'ibeacon_major', - 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', 'can_report_mistake', - 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', 'enter_description', - 'level_change_description', 'base_mapdata_accessible', 'label_settings', 'label_override', 'min_zoom', - 'max_zoom', 'font_size', 'members', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', + "beacon_type", 'level_index', 'short_label', 'origin_space', 'target_space', 'data', "ap_name", 'comment', + 'slow_down_factor', 'groundaltitude', 'node_number', 'addresses', 'bluetooth_address', 'group', + 'ibeacon_uuid', 'ibeacon_major', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', + 'can_report_mistake', 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', + 'enter_description', 'level_change_description', 'base_mapdata_accessible', 'label_settings', 'label_override', + 'min_zoom', 'max_zoom', 'font_size', 'members', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', 'allow_dynamic_locations', 'left', 'top', 'right', 'bottom', 'import_tag', 'import_block_data', 'import_block_geom', 'public', 'default', 'dark', 'high_contrast', 'funky', 'randomize_primary_color', 'color_logo', 'color_css_initial', 'color_css_primary', 'color_css_secondary', 'color_css_tertiary', diff --git a/src/c3nav/mapdata/management/commands/findbeacons.py b/src/c3nav/mapdata/management/commands/findbeacons.py index 874316e8..2ffc0c38 100644 --- a/src/c3nav/mapdata/management/commands/findbeacons.py +++ b/src/c3nav/mapdata/management/commands/findbeacons.py @@ -19,7 +19,7 @@ class Command(BaseCommand): found_beacons.setdefault(measurement["bssid"], []).append((beacon_measurement, measurement)) # put in the ones we know - known = dict(chain(*(((bssid, r) for bssid in r.wifi_bssids) for r in RangingBeacon.objects.all()))) + known = dict(chain(*(((bssid, r) for bssid in r.addresses) for r in RangingBeacon.objects.all()))) # lets go through them for bssid, measurements in found_beacons.items(): diff --git a/src/c3nav/mapdata/management/commands/importnoc.py b/src/c3nav/mapdata/management/commands/importnoc.py index b8c752b8..a7466f8a 100644 --- a/src/c3nav/mapdata/management/commands/importnoc.py +++ b/src/c3nav/mapdata/management/commands/importnoc.py @@ -48,7 +48,8 @@ class Command(BaseCommand): spaces_for_level.setdefault(space.level_id, []).append(space) beacons_so_far: dict[str, RangingBeacon] = { - **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="noc:")}, + **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="noc:", + beacon_type=RangingBeacon.BeaconType.EVENT_WIFI)}, } for name, item in items.items(): @@ -118,7 +119,7 @@ class Command(BaseCommand): # build resulting object altitude_quest = True if not result: - result = RangingBeacon(import_tag=import_tag) + result = RangingBeacon(import_tag=import_tag, beacon_type=RangingBeacon.BeaconType.EVENT_WIFI) else: if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.03: continue diff --git a/src/c3nav/mapdata/migrations/0136_wifi_bssids_to_addresses_and_more.py b/src/c3nav/mapdata/migrations/0136_wifi_bssids_to_addresses_and_more.py new file mode 100644 index 00000000..e38c88c6 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0136_wifi_bssids_to_addresses_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0.8 on 2024-12-28 14:15 + +import django.core.serializers.json +import django_pydantic_field.compat.django +import django_pydantic_field.fields +import pydantic_extra_types.mac_address +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0135_rangingbeacon_beacon_type'), + ] + + operations = [ + migrations.RenameField( + model_name='rangingbeacon', + old_name='wifi_bssids', + new_name='addresses', + ), + migrations.AlterField( + model_name='rangingbeacon', + name='addresses', + field=django_pydantic_field.fields.PydanticSchemaField(config=None, default=list, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="uses node's value if not set", + schema=django_pydantic_field.compat.django.GenericContainer( + list, + (pydantic_extra_types.mac_address.MacAddress,)), + verbose_name='Mac Address / BSSIDs'), + ), + migrations.AlterField( + model_name='rangingbeacon', + name='ap_name', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='AP name'), + ), + ] diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index 15a7c812..fb818534 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -470,9 +470,10 @@ class BeaconMeasurement(SpaceGeometryMixin, models.Model): for peer in scan: if peer.ap_name: map_name.setdefault(peer.ap_name, []).append(peer.bssid) - for beacon in RangingBeacon.objects.filter(ap_name__in=map_name.keys()): + for beacon in RangingBeacon.objects.filter(ap_name__in=map_name.keys(), + beacon_type=RangingBeacon.BeaconType.EVENT_WIFI): print(beacon, "add ssids", set(map_name[beacon.ap_name])) - beacon.wifi_bssids = list(set(beacon.wifi_bssids) | set(map_name[beacon.ap_name])) + beacon.addresses = list(set(beacon.addresses) | set(map_name[beacon.ap_name])) beacon.save() def save(self, *args, **kwargs): @@ -495,8 +496,8 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): node_number = models.PositiveSmallIntegerField(_('Node Number'), unique=True, null=True, blank=True) - wifi_bssids: list[MacAddress] = SchemaField(list[MacAddress], verbose_name=_('WiFi BSSIDs'), default=list, - help_text=_("uses node's value if not set")) + addresses: list[MacAddress] = SchemaField(list[MacAddress], verbose_name=_('Mac Address / BSSIDs'), default=list, + help_text=_("uses node's value if not set")) bluetooth_address = models.CharField(_('Bluetooth Address'), unique=True, null=True, blank=True, max_length=17, validators=[RegexValidator( @@ -522,7 +523,7 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): altitude = models.DecimalField(_('altitude above ground'), max_digits=6, decimal_places=2, default=0, validators=[MinValueValidator(Decimal('0'))]) - ap_name = models.TextField(null=True, blank=True, verbose_name=_('AP name')) + ap_name = models.CharField(null=True, blank=True, verbose_name=_('AP name'), max_length=32) comment = models.TextField(null=True, blank=True, verbose_name=_('comment')) altitude_quest = models.BooleanField(_('altitude quest'), default=True) @@ -551,8 +552,8 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): title = ' - '.join(segments).strip() else: title = f'#{self.pk}' - if self.wifi_bssids: - ssids = self.wifi_bssids[0] + (', …' if len(self.wifi_bssids) > 1 else '') + if self.addresses: + ssids = self.addresses[0] + (', …' if len(self.adresses) > 1 else '') title += f' ({ssids})' if self.comment: title += f' ({self.comment})' diff --git a/src/c3nav/mapdata/quests/positioning.py b/src/c3nav/mapdata/quests/positioning.py index c1b1a9c1..f3f495cd 100644 --- a/src/c3nav/mapdata/quests/positioning.py +++ b/src/c3nav/mapdata/quests/positioning.py @@ -61,17 +61,17 @@ class RangingBeaconBSSIDsQuestForm(ChangeSetModelForm): super().__init__(*args, **kwargs) self.fields["look_for_ap"] = CharField(disabled=True, initial=self.instance.import_tag[4:], widget=HiddenInput()) - self.fields["wifi_bssids"].widget = HiddenInput() + self.fields["addresses"].widget = HiddenInput() - def clean_bssids(self): - data = self.cleaned_data["wifi_bssids"] + def clean_addresses(self): + data = self.cleaned_data["addresses"] if not data: raise ValidationError(_("Need at least one bssid.")) return data class Meta: model = RangingBeacon - fields = ("wifi_bssids", ) + fields = ("addresses", ) @property def changeset_title(self): @@ -103,7 +103,7 @@ class RangingBeaconBSSIDsQuest(Quest): @classmethod def _qs_for_request(cls, request): - return RangingBeacon.qs_for_request(request).filter(ap_name__isnull=False, wifi_bssids=[]) + return RangingBeacon.qs_for_request(request).filter(ap_name__isnull=False, addresses=[]) class BeaconMeasurementQuestForm(ChangeSetModelForm): diff --git a/src/c3nav/mesh/api.py b/src/c3nav/mesh/api.py index 86a23076..47f591ea 100644 --- a/src/c3nav/mesh/api.py +++ b/src/c3nav/mesh/api.py @@ -272,7 +272,7 @@ def mesh_map(request, level_id: int): "geometry": mapping(beacon.geometry), "properties": { "node_number": beacon.node_number, - "wifi_bssid": (beacon.wifi_bssids + [None])[0], + "wifi_bssid": (beacon.addresses + [None])[0], "comment": beacon.comment, "mesh_node": None if node is None else { "address": node.address, diff --git a/src/c3nav/mesh/models.py b/src/c3nav/mesh/models.py index b7e8c33a..3dd084b6 100644 --- a/src/c3nav/mesh/models.py +++ b/src/c3nav/mesh/models.py @@ -197,10 +197,10 @@ class MeshNodeQuerySet(models.QuerySet): for ranging_beacon in RangingBeacon.objects.filter( Q(node_number__in=nodes_by_id.keys()) ).select_related('space'): - if not (set(ranging_beacon.wifi_bssids) & nodes_by_bssid_keys): + if not (set(ranging_beacon.addresses) & nodes_by_bssid_keys): continue # noinspection PyUnresolvedReferences - for bssid in ranging_beacon.wifi_bssids: + for bssid in ranging_beacon.addresses: with suppress(KeyError): nodes_by_bssid[bssid]._ranging_beacon = ranging_beacon with suppress(KeyError): diff --git a/src/c3nav/mesh/utils.py b/src/c3nav/mesh/utils.py index 0369822b..d2d48d90 100644 --- a/src/c3nav/mesh/utils.py +++ b/src/c3nav/mesh/utils.py @@ -60,8 +60,8 @@ def get_nodes_and_ranging_beacons(): ranging_beacon = beacons[ranging_beacon_id] ranging_beacon.save = None - if not ranging_beacon.wifi_bssids: - ranging_beacon.wifi_bssids = [node.address] + if not ranging_beacon.addresses: + ranging_beacon.addresses = [node.address] if not ranging_beacon.bluetooth_address: ranging_beacon.bluetooth_address = node.address[:-2] + hex(int(node.address[-2:], 16)+1)[2:] diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index a25130e9..697f1d8a 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -94,7 +94,7 @@ class Locator: calculated = get_nodes_and_ranging_beacons() for beacon in calculated.beacons.values(): identifiers = [] - for bssid in beacon.wifi_bssids: + for bssid in beacon.addresses: identifiers.append(TypedIdentifier(PeerType.WIFI, bssid)) if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None: identifiers.append( diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 22b12eb6..248b7db5 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1486,7 +1486,7 @@ c3nav = { const $modal = $('#modal'); const $match_ap = $modal.find('[name=look_for_ap]'); if ($match_ap.length) { - const $wifi_bssids = $('[name=wifi_bssids]'); + const $addresses = $('[name=addresses]'); const ap_name = $match_ap.val(); const found_bssids = {}; let scan_complete = false; @@ -1515,7 +1515,7 @@ c3nav = { if (scan_complete) { // todo only bssids that have count > 1 - $wifi_bssids.val(JSON.stringify(Object.keys(found_bssids))); + $addresses.val(JSON.stringify(Object.keys(found_bssids))); $('#modal button[type=submit]').show(); } } From 6d78a0c491d82c66afe6a678c9080c297dd1fa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 15:35:15 +0100 Subject: [PATCH 12/63] pointimporthelper --- .../mapdata/management/commands/importnoc.py | 74 ++++--------------- src/c3nav/mapdata/tasks.py | 5 +- src/c3nav/mapdata/utils/importer.py | 65 ++++++++++++++++ 3 files changed, 82 insertions(+), 62 deletions(-) create mode 100644 src/c3nav/mapdata/utils/importer.py diff --git a/src/c3nav/mapdata/management/commands/importnoc.py b/src/c3nav/mapdata/management/commands/importnoc.py index a7466f8a..c2d187ab 100644 --- a/src/c3nav/mapdata/management/commands/importnoc.py +++ b/src/c3nav/mapdata/management/commands/importnoc.py @@ -1,17 +1,14 @@ -import hashlib - import requests from django.conf import settings from django.core.management.base import BaseCommand from pydantic import BaseModel from shapely import distance -from c3nav.mapdata.models import MapUpdate, Space, Level +from c3nav.mapdata.models import MapUpdate from c3nav.mapdata.models.geometry.space import RangingBeacon -from c3nav.mapdata.models.report import Report +from c3nav.mapdata.utils.importer import PointImportHelper from c3nav.mapdata.utils.cache.changes import changed_geometries from c3nav.mapdata.utils.geometry import unwrap_geom -from shapely.ops import nearest_points, unary_union class NocImportItem(BaseModel): @@ -40,12 +37,7 @@ class Command(BaseCommand): MapUpdate.objects.create(type='importnoc') def do_import(self, items: dict[str, NocImportItem]): - spaces_for_level = {} - levels = tuple(Level.objects.values_list("pk", flat=True)) - lower_levels_for_level = {pk: levels[:i] for i, pk in enumerate(levels)} - - for space in Space.objects.select_related('level').prefetch_related('holes'): - spaces_for_level.setdefault(space.level_id, []).append(space) + import_helper = PointImportHelper() beacons_so_far: dict[str, RangingBeacon] = { **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="noc:", @@ -64,54 +56,16 @@ class Command(BaseCommand): print(f"ERROR: {name} has invalid layer: {item.layer}") continue - new_geometry = converter.convert(item.lat, item.lng) + point = converter.convert(item.lat, item.lng) - # determine space - possible_spaces = [space for space in spaces_for_level[converter.level_id] - if space.geometry.intersects(new_geometry)] - if not possible_spaces: - possible_spaces = [space for space in spaces_for_level[converter.level_id] - if distance(unwrap_geom(space.geometry), new_geometry) < 0.3] - if len(possible_spaces) == 1: - new_space = possible_spaces[0] - the_distance = distance(unwrap_geom(new_space.geometry), new_geometry) - print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}") - elif len(possible_spaces) == 2: - new_space = min(possible_spaces, key=lambda s: distance(unwrap_geom(s.geometry), new_geometry)) - print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}...") - else: - print(f"ERROR: {name} is not within any space (NOC: {(item.lat, item.lng)}, NAV: {new_geometry}") - continue + new_space, point = import_helper.get_point_and_space( + level_id=converter.level_id, + point=point, + name=name, + ) - # move point into space if needed - new_space_geometry = new_space.geometry.difference( - unary_union([unwrap_geom(hole.geometry) for hole in new_space.columns.all()]) - ) - if not new_space_geometry.intersects(new_geometry): - new_geometry = nearest_points(new_space_geometry.buffer(-0.05), new_geometry)[0] - elif len(possible_spaces) == 1: - new_space = possible_spaces[0] - print(f"SUCCESS: {name} is in {new_space.title}") - else: - print(f"WARNING: {name} could be in multiple spaces, picking one...") - new_space = possible_spaces[0] - - lower_levels = lower_levels_for_level[new_space.level_id] - for lower_level in reversed(lower_levels): - # let's go through the lower levels - if not unary_union([unwrap_geom(h.geometry) for h in new_space.holes.all()]).intersects(new_geometry): - # current selected spacae is fine, that's it - break - print(f"NOTE: {name} is in a hole, looking lower...") - - # find a lower space - possible_spaces = [space for space in spaces_for_level[lower_level] - if space.geometry.intersects(new_geometry)] - if possible_spaces: - new_space = possible_spaces[0] - print(f"NOTE: {name} moved to lower space {new_space}") - else: - print(f"WARNING: {name} couldn't find a lower space, still in a hole") + if new_space is None: + continue # find existing location result = beacons_so_far.pop(import_tag, None) @@ -121,14 +75,14 @@ class Command(BaseCommand): if not result: result = RangingBeacon(import_tag=import_tag, beacon_type=RangingBeacon.BeaconType.EVENT_WIFI) else: - if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.03: + if result.space == new_space and distance(unwrap_geom(result.geometry), point) < 0.03: continue - if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.20: + if result.space == new_space and distance(unwrap_geom(result.geometry), point) < 0.20: altitude_quest = False result.comment = name result.space = new_space - result.geometry = new_geometry + result.geometry = point result.altitude = 0 if altitude_quest: result.altitude_quest = True diff --git a/src/c3nav/mapdata/tasks.py b/src/c3nav/mapdata/tasks.py index 8b1d1836..f264d9ab 100644 --- a/src/c3nav/mapdata/tasks.py +++ b/src/c3nav/mapdata/tasks.py @@ -51,5 +51,6 @@ def process_map_updates(self): @app.task(bind=True, max_retries=10) def delete_map_cache_key(self, cache_key): - for key in cache.keys(f'*{cache_key}*'): - cache.delete(key) + if hasattr(cache, 'keys'): + for key in cache.keys(f'*{cache_key}*'): + cache.delete(key) diff --git a/src/c3nav/mapdata/utils/importer.py b/src/c3nav/mapdata/utils/importer.py new file mode 100644 index 00000000..a756c9f6 --- /dev/null +++ b/src/c3nav/mapdata/utils/importer.py @@ -0,0 +1,65 @@ +from shapely import Point, distance +from shapely.ops import unary_union, nearest_points + +from c3nav.mapdata.models import Level, Space +from c3nav.mapdata.utils.geometry import unwrap_geom + + +class PointImportHelper: + def __init__(self): + self.spaces_for_level = {} + self.levels = tuple(Level.objects.values_list("pk", flat=True)) + self.lower_levels_for_level = {pk: self.levels[:i] for i, pk in enumerate(self.levels)} + + for space in Space.objects.select_related('level').prefetch_related('holes'): + self.spaces_for_level.setdefault(space.level_id, []).append(space) + + def get_point_and_space(self, level_id: int, point: Point, name: str): + # determine space + possible_spaces = [space for space in self.spaces_for_level[level_id] + if space.geometry.intersects(point)] + if not possible_spaces: + possible_spaces = [space for space in self.spaces_for_level[level_id] + if distance(unwrap_geom(space.geometry), point) < 0.3] + if len(possible_spaces) == 1: + new_space = possible_spaces[0] + the_distance = distance(unwrap_geom(new_space.geometry), point) + print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}") + elif len(possible_spaces) == 2: + new_space = min(possible_spaces, key=lambda s: distance(unwrap_geom(s.geometry), point)) + print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}...") + else: + print(f"ERROR: {name} is not within any space ({point})") + return None, None + + # move point into space if needed + new_space_geometry = new_space.geometry.difference( + unary_union([unwrap_geom(hole.geometry) for hole in new_space.columns.all()]) + ) + if not new_space_geometry.intersects(point): + point = nearest_points(new_space_geometry.buffer(-0.05), point)[0] + elif len(possible_spaces) == 1: + new_space = possible_spaces[0] + print(f"SUCCESS: {name} is in {new_space.title}") + else: + print(f"WARNING: {name} could be in multiple spaces, picking one...") + new_space = possible_spaces[0] + + lower_levels = self.lower_levels_for_level[new_space.level_id] + for lower_level in reversed(lower_levels): + # let's go through the lower levels + if not unary_union([unwrap_geom(h.geometry) for h in new_space.holes.all()]).intersects(point): + # current selected spacae is fine, that's it + break + print(f"NOTE: {name} is in a hole, looking lower...") + + # find a lower space + possible_spaces = [space for space in self.spaces_for_level[lower_level] + if space.geometry.intersects(point)] + if possible_spaces: + new_space = possible_spaces[0] + print(f"NOTE: {name} moved to lower space {new_space}") + else: + print(f"WARNING: {name} couldn't find a lower space, still in a hole") + + return new_space, point From 11d784f080f2718f4a11077dc0016e7d4679c74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 15:42:05 +0100 Subject: [PATCH 13/63] open quest with leading slash to avoid redirect --- src/c3nav/site/static/site/js/c3nav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 248b7db5..c150f50b 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -2893,7 +2893,7 @@ QuestsControl = ExpandingControl.extend({ .addTo(c3nav._questsLayers[quest.level_id]) .on('click', function () { c3nav.open_modal(); - $.get(`/editor/quests/${quest_type}/${quest.identifier}`, c3nav._modal_loaded).fail(c3nav._modal_error); + $.get(`/editor/quests/${quest_type}/${quest.identifier}/`, c3nav._modal_loaded).fail(c3nav._modal_error); }); } From 0f9dcd57226f07d8a4def50bb1d7acf1035455fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 15:57:16 +0100 Subject: [PATCH 14/63] importnoc importpoc --- .../mapdata/management/commands/importnoc.py | 2 +- .../mapdata/management/commands/importpoc.py | 125 ++++++------------ 2 files changed, 44 insertions(+), 83 deletions(-) diff --git a/src/c3nav/mapdata/management/commands/importnoc.py b/src/c3nav/mapdata/management/commands/importnoc.py index c2d187ab..8535ceaf 100644 --- a/src/c3nav/mapdata/management/commands/importnoc.py +++ b/src/c3nav/mapdata/management/commands/importnoc.py @@ -80,7 +80,7 @@ class Command(BaseCommand): if result.space == new_space and distance(unwrap_geom(result.geometry), point) < 0.20: altitude_quest = False - result.comment = name + result.ap_name = name result.space = new_space result.geometry = point result.altitude = 0 diff --git a/src/c3nav/mapdata/management/commands/importpoc.py b/src/c3nav/mapdata/management/commands/importpoc.py index b8c752b8..bff65cbe 100644 --- a/src/c3nav/mapdata/management/commands/importpoc.py +++ b/src/c3nav/mapdata/management/commands/importpoc.py @@ -1,133 +1,94 @@ -import hashlib +from typing import Literal import requests from django.conf import settings from django.core.management.base import BaseCommand from pydantic import BaseModel +from pydantic.type_adapter import TypeAdapter +from pydantic_extra_types.mac_address import MacAddress from shapely import distance +from shapely.geometry import shape, Point -from c3nav.mapdata.models import MapUpdate, Space, Level +from c3nav.api.schema import PointSchema +from c3nav.mapdata.models import MapUpdate, Level from c3nav.mapdata.models.geometry.space import RangingBeacon -from c3nav.mapdata.models.report import Report from c3nav.mapdata.utils.cache.changes import changed_geometries from c3nav.mapdata.utils.geometry import unwrap_geom -from shapely.ops import nearest_points, unary_union +from c3nav.mapdata.utils.importer import PointImportHelper -class NocImportItem(BaseModel): +class PocImportItemProperties(BaseModel): + level: str + mac: MacAddress + name: str + + +class PocImportItem(BaseModel): """ Something imported from the NOC """ - lat: float | int - lng: float | int - layer: str - type: str = "unknown" + type: Literal["Point"] = "Point" + geometry: PointSchema + properties: PocImportItemProperties class Command(BaseCommand): help = 'import APs from noc' def handle(self, *args, **options): - r = requests.get(settings.NOC_BASE+"/api/markers/get") + r = requests.get(settings.POC_BASE+"antenna-locations") r.raise_for_status() - items = {name: NocImportItem.model_validate(item) - for name, item in r.json()["markers"].items() - if not name.startswith("__polyline")} + items = TypeAdapter(list[PocImportItem]).validate_python(r.json()) with MapUpdate.lock(): changed_geometries.reset() self.do_import(items) MapUpdate.objects.create(type='importnoc') - def do_import(self, items: dict[str, NocImportItem]): - spaces_for_level = {} - levels = tuple(Level.objects.values_list("pk", flat=True)) - lower_levels_for_level = {pk: levels[:i] for i, pk in enumerate(levels)} - - for space in Space.objects.select_related('level').prefetch_related('holes'): - spaces_for_level.setdefault(space.level_id, []).append(space) + def do_import(self, items: list[PocImportItem]): + import_helper = PointImportHelper() beacons_so_far: dict[str, RangingBeacon] = { - **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="noc:")}, + **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="poc:", + beacon_type=RangingBeacon.BeaconType.DECT)}, } - for name, item in items.items(): - import_tag = f"noc:{name}" + levels_by_level_index = {str(level.level_index): level for level in Level.objects.all()} - if item.type != "AP": - continue + for item in items: + import_tag = f"poc:{item.properties.name}" # determine geometry - converter = settings.NOC_LAYERS.get(item.layer, None) - if not converter: - print(f"ERROR: {name} has invalid layer: {item.layer}") + level_id = levels_by_level_index[item.properties.level] + + point: Point = shape(item.geometry.model_dump()) # nowa + + new_space, point = import_helper.get_point_and_space( + level_id=level_id, + point=point, + name=item.properties.name, + ) + + if new_space is None: continue - new_geometry = converter.convert(item.lat, item.lng) - - # determine space - possible_spaces = [space for space in spaces_for_level[converter.level_id] - if space.geometry.intersects(new_geometry)] - if not possible_spaces: - possible_spaces = [space for space in spaces_for_level[converter.level_id] - if distance(unwrap_geom(space.geometry), new_geometry) < 0.3] - if len(possible_spaces) == 1: - new_space = possible_spaces[0] - the_distance = distance(unwrap_geom(new_space.geometry), new_geometry) - print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}") - elif len(possible_spaces) == 2: - new_space = min(possible_spaces, key=lambda s: distance(unwrap_geom(s.geometry), new_geometry)) - print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}...") - else: - print(f"ERROR: {name} is not within any space (NOC: {(item.lat, item.lng)}, NAV: {new_geometry}") - continue - - # move point into space if needed - new_space_geometry = new_space.geometry.difference( - unary_union([unwrap_geom(hole.geometry) for hole in new_space.columns.all()]) - ) - if not new_space_geometry.intersects(new_geometry): - new_geometry = nearest_points(new_space_geometry.buffer(-0.05), new_geometry)[0] - elif len(possible_spaces) == 1: - new_space = possible_spaces[0] - print(f"SUCCESS: {name} is in {new_space.title}") - else: - print(f"WARNING: {name} could be in multiple spaces, picking one...") - new_space = possible_spaces[0] - - lower_levels = lower_levels_for_level[new_space.level_id] - for lower_level in reversed(lower_levels): - # let's go through the lower levels - if not unary_union([unwrap_geom(h.geometry) for h in new_space.holes.all()]).intersects(new_geometry): - # current selected spacae is fine, that's it - break - print(f"NOTE: {name} is in a hole, looking lower...") - - # find a lower space - possible_spaces = [space for space in spaces_for_level[lower_level] - if space.geometry.intersects(new_geometry)] - if possible_spaces: - new_space = possible_spaces[0] - print(f"NOTE: {name} moved to lower space {new_space}") - else: - print(f"WARNING: {name} couldn't find a lower space, still in a hole") - # find existing location result = beacons_so_far.pop(import_tag, None) # build resulting object altitude_quest = True if not result: - result = RangingBeacon(import_tag=import_tag) + result = RangingBeacon(import_tag=import_tag, beacon_type=RangingBeacon.BeaconType.EVENT_WIFI) else: - if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.03: + if result.space == new_space and distance(unwrap_geom(result.geometry), point) < 0.03: continue - if result.space == new_space and distance(unwrap_geom(result.geometry), new_geometry) < 0.20: + if result.space == new_space and distance(unwrap_geom(result.geometry), point) < 0.20: altitude_quest = False - result.comment = name + result.ap_name = item.properties.name + result.addresses = [item.properties.mac.lower()] result.space = new_space - result.geometry = new_geometry + result.geometry = point result.altitude = 0 if altitude_quest: result.altitude_quest = True From 5efca75f6fc15b17e26d80f8e88c6fd2196b2189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 15:59:59 +0100 Subject: [PATCH 15/63] add poc api settings --- src/c3nav/mapdata/management/commands/importpoc.py | 2 +- src/c3nav/settings.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/management/commands/importpoc.py b/src/c3nav/mapdata/management/commands/importpoc.py index bff65cbe..cf721497 100644 --- a/src/c3nav/mapdata/management/commands/importpoc.py +++ b/src/c3nav/mapdata/management/commands/importpoc.py @@ -36,7 +36,7 @@ class Command(BaseCommand): help = 'import APs from noc' def handle(self, *args, **options): - r = requests.get(settings.POC_BASE+"antenna-locations") + r = requests.get(settings.POC_BASE+"/antenna-locations", headers={'ApiKey': settings.POC_API_SECRET}) r.raise_for_status() items = TypeAdapter(list[PocImportItem]).validate_python(r.json()) diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index 4724f18e..3cd38f07 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -223,6 +223,9 @@ if not HUB_API_SECRET: NOC_BASE = config.get('c3nav', 'noc_base', fallback='').removesuffix('/') NOC_LAYERS = NocLayersSchema.validate_json(config.get('c3nav', 'noc_layers', fallback='{}')) +POC_API_BASE = config.get('c3nav', 'poc_api_base', fallback='').removesuffix('/') +POC_API_SECRET = config.get('c3nav', 'poc_api_secret', fallback='') + _db_backend = config.get('database', 'backend', fallback='sqlite3') DATABASES: dict[str, dict[str, str | int | Path]] = { 'default': env.db_url('C3NAV_DATABASE') if 'C3NAV_DATABASE' in env else { From ec057be7c19f48d7eb0ee29cc7604bf1489339c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 16:06:03 +0100 Subject: [PATCH 16/63] some more importpoc stuff --- src/c3nav/mapdata/management/commands/importpoc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c3nav/mapdata/management/commands/importpoc.py b/src/c3nav/mapdata/management/commands/importpoc.py index cf721497..cfe6284e 100644 --- a/src/c3nav/mapdata/management/commands/importpoc.py +++ b/src/c3nav/mapdata/management/commands/importpoc.py @@ -27,7 +27,7 @@ class PocImportItem(BaseModel): """ Something imported from the NOC """ - type: Literal["Point"] = "Point" + type: Literal["Feature"] = "Feature" geometry: PointSchema properties: PocImportItemProperties @@ -36,7 +36,7 @@ class Command(BaseCommand): help = 'import APs from noc' def handle(self, *args, **options): - r = requests.get(settings.POC_BASE+"/antenna-locations", headers={'ApiKey': settings.POC_API_SECRET}) + r = requests.get(settings.POC_API_BASE+"/antenna-locations", headers={'ApiKey': settings.POC_API_SECRET}) r.raise_for_status() items = TypeAdapter(list[PocImportItem]).validate_python(r.json()) @@ -59,7 +59,7 @@ class Command(BaseCommand): import_tag = f"poc:{item.properties.name}" # determine geometry - level_id = levels_by_level_index[item.properties.level] + level_id = levels_by_level_index[item.properties.level].pk point: Point = shape(item.geometry.model_dump()) # nowa From 86a45351114da18a06d39c0d820f821e5e36aa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 17:58:28 +0100 Subject: [PATCH 17/63] improve importpoc some more --- src/c3nav/mapdata/management/commands/importpoc.py | 2 +- src/c3nav/mapdata/models/geometry/space.py | 2 +- src/c3nav/mapdata/utils/importer.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/c3nav/mapdata/management/commands/importpoc.py b/src/c3nav/mapdata/management/commands/importpoc.py index cfe6284e..f0522db1 100644 --- a/src/c3nav/mapdata/management/commands/importpoc.py +++ b/src/c3nav/mapdata/management/commands/importpoc.py @@ -78,7 +78,7 @@ class Command(BaseCommand): # build resulting object altitude_quest = True if not result: - result = RangingBeacon(import_tag=import_tag, beacon_type=RangingBeacon.BeaconType.EVENT_WIFI) + result = RangingBeacon(import_tag=import_tag, beacon_type=RangingBeacon.BeaconType.DECT) else: if result.space == new_space and distance(unwrap_geom(result.geometry), point) < 0.03: continue diff --git a/src/c3nav/mapdata/models/geometry/space.py b/src/c3nav/mapdata/models/geometry/space.py index fb818534..a8d6d4f5 100644 --- a/src/c3nav/mapdata/models/geometry/space.py +++ b/src/c3nav/mapdata/models/geometry/space.py @@ -553,7 +553,7 @@ class RangingBeacon(SpaceGeometryMixin, models.Model): else: title = f'#{self.pk}' if self.addresses: - ssids = self.addresses[0] + (', …' if len(self.adresses) > 1 else '') + ssids = self.addresses[0] + (', …' if len(self.addresses) > 1 else '') title += f' ({ssids})' if self.comment: title += f' ({self.comment})' diff --git a/src/c3nav/mapdata/utils/importer.py b/src/c3nav/mapdata/utils/importer.py index a756c9f6..79481b70 100644 --- a/src/c3nav/mapdata/utils/importer.py +++ b/src/c3nav/mapdata/utils/importer.py @@ -20,16 +20,17 @@ class PointImportHelper: if space.geometry.intersects(point)] if not possible_spaces: possible_spaces = [space for space in self.spaces_for_level[level_id] - if distance(unwrap_geom(space.geometry), point) < 0.3] + if distance(unwrap_geom(space.geometry), point) < 1.5] if len(possible_spaces) == 1: new_space = possible_spaces[0] the_distance = distance(unwrap_geom(new_space.geometry), point) print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}") - elif len(possible_spaces) == 2: + elif len(possible_spaces) > 1: new_space = min(possible_spaces, key=lambda s: distance(unwrap_geom(s.geometry), point)) - print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}...") + print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}, " + f"which is {distance(unwrap_geom(new_space.geometry), point)}m away...") else: - print(f"ERROR: {name} is not within any space ({point})") + print(f"ERROR: {name} is not within any space on level {level_id} ({point})") return None, None # move point into space if needed From 79b21cc6d0cb9ca6c1abd8daa0075a7022c6acd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 18:01:44 +0100 Subject: [PATCH 18/63] check clean_data correctly --- src/c3nav/mapdata/quests/positioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/quests/positioning.py b/src/c3nav/mapdata/quests/positioning.py index f3f495cd..f91b9ef0 100644 --- a/src/c3nav/mapdata/quests/positioning.py +++ b/src/c3nav/mapdata/quests/positioning.py @@ -111,7 +111,7 @@ class BeaconMeasurementQuestForm(ChangeSetModelForm): super().__init__(*args, **kwargs) self.fields["data"].widget = HiddenInput() - def clean_bssids(self): + def clean_data(self): data = self.cleaned_data["data"] if not data: raise ValidationError(_("Need at least one scan.")) From f2f11ca2efb5f1e5ffd5294571e61b14dc2271eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 18:09:57 +0100 Subject: [PATCH 19/63] some updates to make quests look nicer --- src/c3nav/mapdata/quests/positioning.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/c3nav/mapdata/quests/positioning.py b/src/c3nav/mapdata/quests/positioning.py index f91b9ef0..4c8533ad 100644 --- a/src/c3nav/mapdata/quests/positioning.py +++ b/src/c3nav/mapdata/quests/positioning.py @@ -15,7 +15,7 @@ class RangingBeaconAltitudeQuestForm(ChangeSetModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["altitude"].label = ( - _('How many meters above ground is the access point “%s” mounted?') % self.instance.comment + _('How many meters above ground is the access point “%s” mounted?') % self.instance.title ) def clean_altitude(self): @@ -59,8 +59,7 @@ class RangingBeaconAltitudeQuest(Quest): class RangingBeaconBSSIDsQuestForm(ChangeSetModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["look_for_ap"] = CharField(disabled=True, initial=self.instance.import_tag[4:], - widget=HiddenInput()) + self.fields["look_for_ap"] = CharField(disabled=True, initial=self.instance.ap_name, widget=HiddenInput()) self.fields["addresses"].widget = HiddenInput() def clean_addresses(self): @@ -91,8 +90,8 @@ class RangingBeaconBSSIDsQuest(Quest): def quest_description(self) -> list[str]: return [ _("This quest only works in the app. It works fully automatically."), - _("We are trying to find the BSSIDs broadcast by “%s”.") % self.obj.ap_name, - _("Please stand near “%s” and wait for the submit button to appear.") % self.obj.ap_name, + _("We are trying to find the BSSIDs broadcast by “%s”.") % self.obj.title, + _("Please stand near “%s” and wait for the submit button to appear.") % self.obj.title, _("Do not close this popup until then."), _("This should happen within less than a minute."), ] From 0beee53e60f16d66f668fe055a9bb89faeb68546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 18:20:42 +0100 Subject: [PATCH 20/63] update german translations --- src/c3nav/locale/de/LC_MESSAGES/django.po | 67 ++++++++++++++++++++--- src/c3nav/mapdata/quests/positioning.py | 7 ++- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/c3nav/locale/de/LC_MESSAGES/django.po b/src/c3nav/locale/de/LC_MESSAGES/django.po index aaab6bef..3f888daa 100644 --- a/src/c3nav/locale/de/LC_MESSAGES/django.po +++ b/src/c3nav/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-27 13:48+0100\n" -"PO-Revision-Date: 2024-12-27 14:44+0100\n" +"POT-Creation-Date: 2024-12-28 18:12+0100\n" +"PO-Revision-Date: 2024-12-28 18:20+0100\n" "Last-Translator: Laura Klünder \n" "Language-Team: \n" "Language: de\n" @@ -709,6 +709,10 @@ msgstr "" "Umleitungs-Slug „%s“ kann nicht hinzugefügt werden: Er wird bereits an " "anderer Stelle verwendet." +#: c3nav/editor/forms.py +msgid "Why is there WiFi scan data if this is a fill quest?" +msgstr "Warum sind WLAN daten in einem Füll Quest?" + #: c3nav/editor/forms.py msgid "WiFi scan data is missing." msgstr "WiFI Scandaten fehlen." @@ -2124,6 +2128,10 @@ msgstr "Kommentar" msgid "Measurement list" msgstr "Messungsliste" +#: c3nav/mapdata/models/geometry/space.py +msgid "create a quest to fill this" +msgstr "Füll-Quest erstellen" + #: c3nav/mapdata/models/geometry/space.py msgid "Beacon Measurement" msgstr "Beacon Messung" @@ -2132,13 +2140,25 @@ msgstr "Beacon Messung" msgid "Beacon Measurements" msgstr "Beacon Messungen" +#: c3nav/mapdata/models/geometry/space.py +msgid "Event WiFi AP" +msgstr "Event-WLAN AP" + +#: c3nav/mapdata/models/geometry/space.py +msgid "DECT antenna" +msgstr "DECT Antenne" + +#: c3nav/mapdata/models/geometry/space.py +msgid "beacon type" +msgstr "Beacon-Typ" + #: c3nav/mapdata/models/geometry/space.py msgid "Node Number" msgstr "Node Nummer" #: c3nav/mapdata/models/geometry/space.py -msgid "WiFi BSSIDs" -msgstr "WLAN BSSIDs" +msgid "Mac Address / BSSIDs" +msgstr "Mac-Adresse / BSSIDs" #: c3nav/mapdata/models/geometry/space.py msgid "uses node's value if not set" @@ -2164,6 +2184,10 @@ msgstr "iBeacon minor value" msgid "UWB Address" msgstr "UWB Adresse" +#: c3nav/mapdata/models/geometry/space.py +msgid "AP name" +msgstr "AP Name" + #: c3nav/mapdata/models/geometry/space.py msgid "altitude quest" msgstr "Höhenquest" @@ -2976,12 +3000,12 @@ msgstr "Kartenänderung" #: c3nav/mapdata/quests/positioning.py #, python-format -msgid "How many meters above ground is the access point “%s” mounted?" -msgstr "Wie viele meter über dem Boden ist der access point „%s“ befestigt?" +msgid "How many meters above ground is “%s” mounted?" +msgstr "Wie viele meter über dem Boden ist „%s“ befestigt?" #: c3nav/mapdata/quests/positioning.py -msgid "The AP should not be 0m above ground." -msgstr "Der AP sollte sich nicht 0m über dem Boden befinden." +msgid "The device should not be 0m above ground." +msgstr "Das Gerät sollte sich nicht 0m über dem Boden befinden." #: c3nav/mapdata/quests/positioning.py msgid "Ranging Beacon Altitude" @@ -3018,6 +3042,30 @@ msgstr "Bitte lass diesen Dialog bis dahin offen." msgid "This should happen within less than a minute." msgstr "Dies sollte weniger als eine Minute dauern." +#: c3nav/mapdata/quests/positioning.py +msgid "Need at least one scan." +msgstr "Mindestens ein scan wird benötigt." + +#: c3nav/mapdata/quests/positioning.py +msgid "Wifi/BLE Positioning" +msgstr "WLAN/BLE-Ortung" + +#: c3nav/mapdata/quests/positioning.py +msgid "" +"Please stand as close to the given location as possible. Feel free to close " +"this window again to double-check." +msgstr "" +"Bitte stehe so nah wie möglcih an dem angezeigten Punkt. Du kannst dieses " +"Fenster schließen um dir die Position nochmal anzugucken." + +#: c3nav/mapdata/quests/positioning.py +msgid "" +"When you're ready, please click the button below and wait for measurements " +"to arrive." +msgstr "" +"Wenn du bereit bist, klicke auf den Butten unten und warte darauf, dass " +"Messwerte erscheinen." + #: c3nav/mapdata/quests/route_descriptions.py msgid "Does this space qualify as “easily identifyable/findable”?" msgstr "Ist dieser Raum „einfach zu identifizieren/finden“?" @@ -4614,6 +4662,9 @@ msgstr "" "API-Secret erstellt. Notier es dir sofort, denn es wird nicht erneut " "angezeigt!" +#~ msgid "WiFi BSSIDs" +#~ msgstr "WLAN BSSIDs" + #~ msgid "WiFi scan data is not a list." #~ msgstr "WifFi Scandaten sind keine Liste." diff --git a/src/c3nav/mapdata/quests/positioning.py b/src/c3nav/mapdata/quests/positioning.py index 4c8533ad..92bb0956 100644 --- a/src/c3nav/mapdata/quests/positioning.py +++ b/src/c3nav/mapdata/quests/positioning.py @@ -15,13 +15,13 @@ class RangingBeaconAltitudeQuestForm(ChangeSetModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["altitude"].label = ( - _('How many meters above ground is the access point “%s” mounted?') % self.instance.title + _('How many meters above ground is “%s” mounted?') % self.instance.title ) def clean_altitude(self): data = self.cleaned_data["altitude"] if not data: - raise ValidationError(_("The AP should not be 0m above ground.")) + raise ValidationError(_("The device should not be 0m above ground.")) return data class Meta: @@ -102,7 +102,8 @@ class RangingBeaconBSSIDsQuest(Quest): @classmethod def _qs_for_request(cls, request): - return RangingBeacon.qs_for_request(request).filter(ap_name__isnull=False, addresses=[]) + return RangingBeacon.qs_for_request(request).filter(ap_name__isnull=False, addresses=[], + beacon_type=RangingBeacon.BeaconType.EVENT_WIFI) class BeaconMeasurementQuestForm(ChangeSetModelForm): From b977746a764aa6be4ab13e8d639b03b7e14c5433 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Sat, 28 Dec 2024 18:53:43 +0100 Subject: [PATCH 21/63] beacon measurement quest UI --- src/c3nav/mapdata/quests/positioning.py | 1 + src/c3nav/site/static/site/css/c3nav.scss | 10 +- src/c3nav/site/static/site/js/c3nav.js | 117 +++++++++++++++++++++- 3 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/c3nav/mapdata/quests/positioning.py b/src/c3nav/mapdata/quests/positioning.py index 92bb0956..f511fcf0 100644 --- a/src/c3nav/mapdata/quests/positioning.py +++ b/src/c3nav/mapdata/quests/positioning.py @@ -109,6 +109,7 @@ class RangingBeaconBSSIDsQuest(Quest): class BeaconMeasurementQuestForm(ChangeSetModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.fields["beacon_measurement_quest"] = CharField(disabled=True, initial='', widget=HiddenInput(), required=False) self.fields["data"].widget = HiddenInput() def clean_data(self): diff --git a/src/c3nav/site/static/site/css/c3nav.scss b/src/c3nav/site/static/site/css/c3nav.scss index 050f79f6..8733d72d 100644 --- a/src/c3nav/site/static/site/css/c3nav.scss +++ b/src/c3nav/site/static/site/css/c3nav.scss @@ -2000,12 +2000,18 @@ blink { } } +.beacon-quest-scanner { + margin-bottom: 1rem; +} -.ap-name-bssid-result { +.ap-name-bssid-result, .beacon-quest-scanner > table { + display: block; + max-height: 30vh; + overflow: scroll; border-radius: 4px; border: 1px solid gray; padding: 4px 0; - box-shadow: inset 0px 0px 1px gray; + box-shadow: inset 0 0 1px gray; thead { border-bottom: 1px solid gray; diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index c150f50b..0a59c6bd 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1471,17 +1471,112 @@ c3nav = { }, _set_modal_content: function (content, no_close) { const $modal = $('#modal'); + const $content = $modal.find('#modal-content'); $modal.toggleClass('loading', !content) - .find('#modal-content') - .html((!no_close) ? '' : '') + $content.html((!no_close) ? '' : '') .append(content || '
'); - if ($modal.find('[name=look_for_ap]').length) { + if ($content.find('[name=look_for_ap]').length) { + $content.find('button[type=submit]').hide(); if (!window.mobileclient) { - alert('need app!') + $content.find('p, form').remove(); + $content.append('

This quest is only available in the android app.

'); // TODO translate + } else { + c3nav._ap_name_scan_result_update(); + } + } else if ($content.find('[name=beacon_measurement_quest]').length) { + $content.find('button[type=submit]').hide(); + if (!window.mobileclient) { + $content.find('p, form').remove(); + $content.append('

This quest is only available in the android app.

'); // TODO translate + } else { + const $scanner = $('
'); + const $button = $('') + .click(() => { + $button.remove(); + $scanner.append('

Scanning… Please do not close this popup and do not move.

'); + c3nav._quest_wifi_scans = []; + c3nav._beacon_quest_scanning = true; + }) + $scanner.append($button); + $content.find('form').prev().after($scanner) } - $modal.find('button').hide(); } }, + _quest_wifi_scans: [], + _quest_ibeacon_scans: [], + _wifi_measurement_scan_update: function () { + + const wifi_display_results = []; + const bluetooth_display_results = []; + for (const scan of c3nav._quest_wifi_scans) { + for (const peer of scan) { + let found = false; + for (const existing_peer of wifi_display_results) { + if (peer.bssid === existing_peer.bssid && peer.ssid === existing_peer.ssid) { + existing_peer.rssi = peer.rssi; + found = true; + break; + } + } + if (!found) { + wifi_display_results.push(peer); + } + } + } + for (const scan of c3nav._quest_ibeacon_scans) { + for (const peer of scan) { + let found = false; + for (const existing_peer of bluetooth_display_results) { + if (peer.uuid === existing_peer.uuid && peer.major === existing_peer.major && peer.minor === existing_peer.minor) { + existing_peer.distance = peer.distance; + found = true; + break; + } + } + if (!found) { + bluetooth_display_results.push(peer); + } + } + } + + const $scanner = $('#modal .beacon-quest-scanner'); + + const $wifi_table = $(`
${c3nav._quest_wifi_scans.length} wifi scans
BSSIDSSIDRSSI
`); + + for (const peer of wifi_display_results) { + $wifi_table.append(`${peer.bssid}${peer.ssid}${peer.rssi}`); + } + + + const $bluetooth_table = $(`
${c3nav._quest_ibeacon_scans.length} wifi scans
IDDistance
`); + + for (const peer of bluetooth_display_results) { + $bluetooth_table.append(`${peer.major}${peer.minor}${peer.distance}`); + } + + + $scanner.empty(); + if (c3nav._quest_wifi_scans.length < 2) { + $scanner.append('

Scanning… Please do not close this popup and do not move.

'); + } else { + if (c3nav._quest_wifi_scans.length > 2) { + $('#modal input[name=data]').val(JSON.stringify({ + wifi: c3nav._quest_wifi_scans, + ibeacon: c3nav._quest_ibeacon_scans, + })) + $('#modal button[type=submit]').show(); + } + } + + if (wifi_display_results.length > 0) { + $scanner.append($wifi_table); + } + if (bluetooth_display_results.length > 0) { + $scanner.append($bluetooth_table); + } + + }, + _ap_name_scan_result_update: function () { const $modal = $('#modal'); const $match_ap = $modal.find('[name=look_for_ap]'); @@ -2178,6 +2273,7 @@ c3nav = { _last_ibeacon_peers: [], _no_scan_count: 0, _ap_name_mappings: {}, + _beacon_quest_scan_results: {}, _enable_scan_debugging: false, _scan_debugging_results: [], _wifi_scan_results: function (peers) { @@ -2221,12 +2317,23 @@ c3nav = { c3nav._ap_name_scan_result_update(); + if (c3nav._beacon_quest_scanning) { + c3nav._quest_wifi_scans.push(peers); + c3nav._wifi_measurement_scan_update(); + } + c3nav._last_wifi_peers = peers; c3nav._after_scan_results(); }, _ibeacon_scan_results: function (peers) { peers = JSON.parse(peers); c3nav._last_ibeacon_peers = peers; + + if (c3nav._beacon_quest_scanning) { + c3nav._quest_ibeacon_scans.push(peers); + c3nav._wifi_measurement_scan_update(); + } + c3nav._after_scan_results(); }, _after_scan_results: function () { From e55ea84477a06bc8089097c739c4c6026f402b38 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Sat, 28 Dec 2024 19:53:54 +0100 Subject: [PATCH 22/63] fix double nearby locations issue --- src/c3nav/site/static/site/js/c3nav.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 0a59c6bd..4349fd70 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1975,21 +1975,23 @@ c3nav = { c3nav._visible_map_locations = []; if (origin) c3nav._merge_bounds(bounds, c3nav._add_location_to_map(origin, single ? c3nav.icons.default : c3nav.icons.origin)); if (destination) c3nav._merge_bounds(bounds, c3nav._add_location_to_map(destination, single ? c3nav.icons.default : c3nav.icons.destination)); - const done = []; + const done = new Set(); if (c3nav.state.nearby && destination && 'areas' in destination) { if (destination.space) { + done.add(destination.space); c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[destination.space], c3nav.icons.nearby, true)); } if (destination.near_area) { - done.push(destination.near_area); + done.add(destination.near_area); c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[destination.near_area], c3nav.icons.nearby, true)); } for (const area of destination.areas) { - done.push(area); + if (done.has(area)) continue; + done.add(area); c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[area], c3nav.icons.nearby, true)); } for (const location of destination.nearby) { - if (location in done) continue; + if (done.has(location)) continue; c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[location], c3nav.icons.nearby, true)); } } From c78e9ed600da8063356ac80c75f49a21317d6bbf Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Sat, 28 Dec 2024 19:55:09 +0100 Subject: [PATCH 23/63] only require one wifi scan to submit quest --- src/c3nav/site/static/site/js/c3nav.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 4349fd70..23d39949 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1556,16 +1556,14 @@ c3nav = { $scanner.empty(); - if (c3nav._quest_wifi_scans.length < 2) { + if (c3nav._quest_wifi_scans.length < 1) { $scanner.append('

Scanning… Please do not close this popup and do not move.

'); } else { - if (c3nav._quest_wifi_scans.length > 2) { - $('#modal input[name=data]').val(JSON.stringify({ - wifi: c3nav._quest_wifi_scans, - ibeacon: c3nav._quest_ibeacon_scans, - })) - $('#modal button[type=submit]').show(); - } + $('#modal input[name=data]').val(JSON.stringify({ + wifi: c3nav._quest_wifi_scans, + ibeacon: c3nav._quest_ibeacon_scans, + })) + $('#modal button[type=submit]').show(); } if (wifi_display_results.length > 0) { From fae4c6f1484b79a045fdaa4d6f01b26340ef7409 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Sat, 28 Dec 2024 20:18:58 +0100 Subject: [PATCH 24/63] kinda-working overlay search --- src/c3nav/site/static/site/js/c3nav.js | 89 +++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 23d39949..efd338b4 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1314,9 +1314,30 @@ c3nav = { }, _locationinput_click_suggestion: function () { const $locationinput = $('#' + c3nav.current_locationinput); - c3nav._locationinput_set($locationinput, c3nav.locations_by_id[$(this).attr('data-id')]); - c3nav.update_state(); - c3nav.fly_to_bounds(true); + const $this = $(this); + const locationId = $this.attr('data-id'); + if (locationId) { + c3nav._locationinput_set($locationinput, c3nav.locations_by_id[$(this).attr('data-id')]); + c3nav.update_state(); + c3nav.fly_to_bounds(true); + } else { + const overlayId = $this.attr('data-overlay-id'); + if (overlayId) { + const featureId = $this.attr('data-feature-id'); + + const overlay = c3nav._overlayControl._overlays[overlayId]; + const featureLayer = overlay.feature_layers[featureId]; + const feature = overlay.features_by_id[featureId]; + const bounds = featureLayer.getBounds(); + c3nav.update_map_state(true, feature.level_id, bounds.getCenter(), c3nav.map.getZoom()); + c3nav._locationLayerBounds = {[feature.level_id]: bounds}; + c3nav.fly_to_bounds(true); + featureLayer.fire('click'); + + c3nav._locationinput_clear(); + } + } + }, _locationinput_matches_compare: function (a, b) { if (a[1] !== b[1]) return b[1] - a[1]; @@ -1387,6 +1408,11 @@ c3nav = { matches.push([location.elem, leading_words_count, words_total_count, words_start_count, -location.title.length, i]) } + for (const overlay of c3nav.activeOverlays()) { + matches.push(...overlay.search(val_words)); + } + + matches.sort(c3nav._locationinput_matches_compare); $autocomplete.html(''); @@ -2218,6 +2244,11 @@ c3nav = { c3nav._overlayControl = control.addTo(c3nav.map); } }, + + activeOverlays: function () { + return Object.values(c3nav._overlayControl._overlays).filter(o => o.active); + }, + _update_quests: function () { if (!c3nav.map) return; if (c3nav._questsControl) { @@ -3339,9 +3370,13 @@ L.SquareGridLayer = L.Layer.extend({ class DataOverlay { levels = null; + features = []; + features_by_id = {}; + feature_layers = {}; feature_geometries = {}; fetch_timeout = null; etag = null; + active = false; constructor(options) { this.id = options.id; @@ -3365,6 +3400,7 @@ class DataOverlay { c3nav_api.get(`mapdata/dataoverlayfeaturegeometries/?overlay=${this.id}`) ]); this.etag = etag; + this.features = features; this.feature_geometries = Object.fromEntries(feature_geometries.map(f => [f.id, f.geometry])); @@ -3407,7 +3443,11 @@ class DataOverlay { this.levels[id].clearLayers(); } + this.feature_layers = {}; + this.features_by_id = {}; + for (const feature of features) { + this.features_by_id[feature.id] = feature; const geometry = this.feature_geometries[feature.id] const level_id = feature.level_id; if (!(level_id in this.levels)) { @@ -3470,6 +3510,8 @@ class DataOverlay { } }); + this.feature_layers[feature.id] = layer; + this.levels[level_id].addLayer(layer); } } @@ -3486,9 +3528,11 @@ class DataOverlay { levels[id].addLayer(this.levels[id]); } } + this.active = true; } disable(levels) { + this.active = false; for (const id in levels) { if (id in this.levels) { levels[id].removeLayer(this.levels[id]); @@ -3497,4 +3541,43 @@ class DataOverlay { window.clearTimeout(this.fetch_timeout); this.fetch_timeout = null; } + + search(words) { + const feature_matches = (feature, word) => { + if (feature.title.toLowerCase().includes(word)) return true; + for (const lang in feature.titles) { + if (feature.titles[lang].toLowerCase().includes(word)) return true; + } + for (const key in feature.extra_data) { + if (`${feature.extra_data[key]}`.toLowerCase().includes(word)) return true; + } + return false; + } + + const matches = []; + + for (const feature of this.features) { + let nomatch = false; + for (const word of words) { + if (this.title.toLowerCase().includes(word)) continue; + + if (!feature_matches(feature, word)) { + nomatch = true; + } + } + if (nomatch) continue; + + const html = $('
') + .append($('').text(c3nav._map_material_icon(feature.point_icon ?? 'place'))) + .append($('').text(feature.title)) + .append($('').text(`${this.title} (Overlay)`)) + .attr('data-overlay-id', this.id) + .attr('data-feature-id', feature.id); + html.attr('data-location', JSON.stringify(location)); + + matches.push([html[0].outerHTML, 0, 0, 0, -feature.title.length, 0]) + } + + return matches; + } } \ No newline at end of file From 7cc097380036ee542a79dc57925205395e42a5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 20:59:29 +0100 Subject: [PATCH 25/63] fix 500 in locate --- src/c3nav/routing/locator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 697f1d8a..9a0a51f0 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -244,10 +244,12 @@ class Locator: (peer_id, value) for peer_id, value in scan_data_we_can_use if self.peers[peer_id].space_id == space_id ], key=lambda a: a[1].rssi) + router = Router.load() + if len(scan_data_in_the_same_room) == 1: - point = space.point + point = router.spaces[space.pk].point return CustomLocation( - level=space.level_id, + level=router.spaces[space.pk].level_id, x=point.x, y=point.y, permissions=permissions, @@ -262,7 +264,7 @@ class Locator: x = self.peers[peer_id].xyz[0] * value.rssi / the_sum y = self.peers[peer_id].xyz[1] * value.rssi / the_sum return CustomLocation( - level=space.level_id, + level=router.spaces[space.pk].level_id, x=x, y=y, permissions=permissions, From 17166eaa895babf6aae861927244a4447ddac932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 21:07:03 +0100 Subject: [PATCH 26/63] fix locate_by_beacon_position some more --- src/c3nav/mapdata/management/commands/importhub.py | 3 --- src/c3nav/routing/locator.py | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/c3nav/mapdata/management/commands/importhub.py b/src/c3nav/mapdata/management/commands/importhub.py index 32e68e81..a8570756 100644 --- a/src/c3nav/mapdata/management/commands/importhub.py +++ b/src/c3nav/mapdata/management/commands/importhub.py @@ -1,11 +1,9 @@ import hashlib -from typing import Literal, Optional from uuid import UUID import requests from django.conf import settings from django.core.management.base import BaseCommand -from django.utils.text import slugify from pydantic import BaseModel, Field, PositiveInt from shapely import Point from shapely.geometry import shape @@ -13,7 +11,6 @@ from shapely.geometry import shape from c3nav.api.utils import NonEmptyStr from c3nav.mapdata.models import Area, LocationGroup, LocationSlug, MapUpdate, Space from c3nav.mapdata.models.geometry.space import POI -from c3nav.mapdata.models.locations import LocationRedirect from c3nav.mapdata.models.report import Report from c3nav.mapdata.utils.cache.changes import changed_geometries diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 9a0a51f0..cdaf310b 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -249,7 +249,7 @@ class Locator: if len(scan_data_in_the_same_room) == 1: point = router.spaces[space.pk].point return CustomLocation( - level=router.spaces[space.pk].level_id, + level=router.levels[router.spaces[space.pk].level_id], x=point.x, y=point.y, permissions=permissions, @@ -264,7 +264,7 @@ class Locator: x = self.peers[peer_id].xyz[0] * value.rssi / the_sum y = self.peers[peer_id].xyz[1] * value.rssi / the_sum return CustomLocation( - level=router.spaces[space.pk].level_id, + level=router.levels[router.spaces[space.pk].level_id], x=x, y=y, permissions=permissions, From 183a7ec4c9a8215a93b94b03288e1aaf52f7e264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 21:16:11 +0100 Subject: [PATCH 27/63] divide by 100 --- src/c3nav/routing/locator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index cdaf310b..f774b06b 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -261,12 +261,12 @@ class Locator: x = 0 y = 0 for peer_id, value in scan_data_in_the_same_room[:3]: - x = self.peers[peer_id].xyz[0] * value.rssi / the_sum - y = self.peers[peer_id].xyz[1] * value.rssi / the_sum + x = float(self.peers[peer_id].xyz[0]) * value.rssi / the_sum + y = float(self.peers[peer_id].xyz[1]) * value.rssi / the_sum return CustomLocation( level=router.levels[router.spaces[space.pk].level_id], - x=x, - y=y, + x=x/100, + y=y/100, permissions=permissions, icon='my_location' ) From 6dafb009fc3bc89c1519f2e4d7b8f1528b1a6964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 22:16:25 +0100 Subject: [PATCH 28/63] passive api name collecting from users that have consented to it --- src/c3nav/control/models.py | 1 + src/c3nav/mapdata/tasks.py | 24 ++++++++++++++++++++++++ src/c3nav/routing/api/positioning.py | 12 +++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/c3nav/control/models.py b/src/c3nav/control/models.py index 856e25aa..9549d18c 100644 --- a/src/c3nav/control/models.py +++ b/src/c3nav/control/models.py @@ -44,6 +44,7 @@ class UserPermissions(models.Model): nonpublic_themes = models.BooleanField(default=False, verbose_name=_('show non-public themes in theme selector')) quests: list[str] = SchemaField(schema=list[str], default=list) impolite_quests = models.BooleanField(default=False, verbose_name=_('dont say thanks after completing a quest')) + passive_ap_name_scanning = models.BooleanField(default=False, verbose_name=_('passive ap name scanning')) class Meta: verbose_name = _('User Permissions') diff --git a/src/c3nav/mapdata/tasks.py b/src/c3nav/mapdata/tasks.py index f264d9ab..8e1aa185 100644 --- a/src/c3nav/mapdata/tasks.py +++ b/src/c3nav/mapdata/tasks.py @@ -54,3 +54,27 @@ def delete_map_cache_key(self, cache_key): if hasattr(cache, 'keys'): for key in cache.keys(f'*{cache_key}*'): cache.delete(key) + + +@app.task(bind=True, max_retries=10) +def update_ap_names_bssid_mapping(self, map_name, user): + from c3nav.mapdata.models.geometry.space import RangingBeacon + todo = [] + for beacon in RangingBeacon.objects.filter(ap_name__in=map_name.keys(), + beacon_type=RangingBeacon.BeaconType.EVENT_WIFI): + print(beacon, "add ssids", set(map_name[beacon.ap_name])) + if set(map_name[beacon.ap_name]) - set(beacon.addresses): + todo.append((beacon, list(set(beacon.addresses) | set(map_name[beacon.ap_name])))) + + if todo: + from c3nav.editor.models import ChangeSet + from c3nav.editor.views.base import within_changeset + changeset = ChangeSet() + changeset.author = user + with within_changeset(changeset=changeset, user=user) as locked_changeset: + for beacon, addresses in todo: + beacon.addresses = addresses + beacon.save() + with changeset.lock_to_edit() as locked_changeset: + locked_changeset.title = 'passive update bssids' + locked_changeset.apply(user) diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index 017a4283..31827c44 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -8,6 +8,7 @@ from pydantic_extra_types.mac_address import MacAddress from c3nav.api.auth import auth_responses from c3nav.api.schema import BaseSchema +from c3nav.editor.models import ChangeSet from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.schemas.models import CustomLocationSchema from c3nav.mapdata.utils.cache.stats import increment_cache_key @@ -40,7 +41,7 @@ class PositioningResult(BaseSchema): description="determine position based on wireless measurements " "(including ranging, if available)", response={200: PositioningResult, **auth_responses}) -def get_position(request, parameters: LocateRequestSchema): +def get_position(request, parameters: LocateRequestSchema, update_ap_names_bssid_mapping=None): try: location = Locator.load().locate(parameters.wifi_peers, permissions=AccessPermission.get_for_request(request)) @@ -51,6 +52,15 @@ def get_position(request, parameters: LocateRequestSchema): # todo: validation error, seriously? this shouldn't happen anyways raise + if request.user_permissions.passive_ap_name_scanning: + bssid_mapping = {} + for peer in parameters.wifi_peers: + if not peer.ap_name: + continue + bssid_mapping.setdefault(peer.ap_name, set()).add(peer.bssid) + if bssid_mapping: + update_ap_names_bssid_mapping.delay(map_name=bssid_mapping, user=request.user) + return { "location": location } From 163e893a43dd7cef192d58b5fe54836dd2dce377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 22:19:31 +0100 Subject: [PATCH 29/63] interpret beacon strength correctly for positioning --- src/c3nav/routing/locator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index f774b06b..4edecc5c 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -236,13 +236,13 @@ class Locator: return None # get visible spaces - best_ap_id = min(scan_data_we_can_use, key=lambda item: item[1].rssi)[0] + best_ap_id = max(scan_data_we_can_use, key=lambda item: item[1].rssi)[0] space_id = self.peers[best_ap_id].space_id space = self.spaces[space_id] scan_data_in_the_same_room = sorted([ (peer_id, value) for peer_id, value in scan_data_we_can_use if self.peers[peer_id].space_id == space_id - ], key=lambda a: a[1].rssi) + ], key=lambda a: -a[1].rssi) router = Router.load() From a2347b6946d9b31d5490733550b02aa5ab8a332b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 22:24:43 +0100 Subject: [PATCH 30/63] better stuffs --- src/c3nav/routing/locator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 4edecc5c..e006f40e 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -256,13 +256,13 @@ class Locator: icon='my_location' ) else: - the_sum = sum(a[1].rssi for a in scan_data_in_the_same_room[:3]) + the_sum = sum((a[1].rssi+90) for a in scan_data_in_the_same_room[:3]) x = 0 y = 0 for peer_id, value in scan_data_in_the_same_room[:3]: - x = float(self.peers[peer_id].xyz[0]) * value.rssi / the_sum - y = float(self.peers[peer_id].xyz[1]) * value.rssi / the_sum + x = float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum + y = float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum return CustomLocation( level=router.levels[router.spaces[space.pk].level_id], x=x/100, From 632fc7319d16b98a2c57cc5de4b0d22f9d52f831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 22:34:02 +0100 Subject: [PATCH 31/63] update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf141aee..e244b022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ and potential backwards incompatibilities. Big stuff: -- Quest support to categorize rooms, find AP altitudes and generate route descriptions +- Quest support to categorize rooms, find AP altitudes, AP names, do wifi scanning and generate route descriptions - data overlay support - complete rewrite of editor changesets as a base for a more modern editor – you will lose all changesets! - new map settings API endpoint - ability to import APs from NOC eventmap +- ability to import Antennas from POC Semi-big stuff: @@ -29,6 +30,9 @@ Semi-big stuff: - support for various SSOs - various compliance checkboxes - support for importing projects and rooms from hub +- match APs using name broadcast in Aruba vendor-data instead of just BSSIDs +- fewer and more performant calls to redis +- pruning redis cache automatically after a new map update is created Small stuff: @@ -37,6 +41,8 @@ Small stuff: - Level short_label has been split into short_label (for displaying) and level_index (for internal use like coordinates) - some API mapdata endpoints were moved, some lesser used properties renamed - proper support for access restricted levels +- ability to store mutiple BSSIDs per beacon +- importhub can now import projects and rooms as well Behind the scenes, comfort, bug fixes: From 4cd268fbb55ea28caea3c6196f2d31ae1d299677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 22:37:37 +0100 Subject: [PATCH 32/63] missing migration <.< --- ...userpermissions_passive_ap_name_scanning.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/c3nav/control/migrations/0019_userpermissions_passive_ap_name_scanning.py diff --git a/src/c3nav/control/migrations/0019_userpermissions_passive_ap_name_scanning.py b/src/c3nav/control/migrations/0019_userpermissions_passive_ap_name_scanning.py new file mode 100644 index 00000000..2bfda63a --- /dev/null +++ b/src/c3nav/control/migrations/0019_userpermissions_passive_ap_name_scanning.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2024-12-28 21:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('control', '0018_userpermissions_impolite_quests'), + ] + + operations = [ + migrations.AddField( + model_name='userpermissions', + name='passive_ap_name_scanning', + field=models.BooleanField(default=False, verbose_name='passive ap name scanning'), + ), + ] From 574ffea67aa0d5af84c094226c83d2aa4d4dc30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 22:51:38 +0100 Subject: [PATCH 33/63] =?UTF-8?q?=3D=20=E2=86=92=20+=3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/c3nav/routing/locator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index e006f40e..2c07d727 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -256,13 +256,13 @@ class Locator: icon='my_location' ) else: - the_sum = sum((a[1].rssi+90) for a in scan_data_in_the_same_room[:3]) + the_sum = sum((value.rssi+90) for peer_id, value in scan_data_in_the_same_room[:3]) x = 0 y = 0 for peer_id, value in scan_data_in_the_same_room[:3]: - x = float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum - y = float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum + x += float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum + y += float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum return CustomLocation( level=router.levels[router.spaces[space.pk].level_id], x=x/100, From f8e88fa8c686fe043bf2fa126efca8bbcc340dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 22:58:25 +0100 Subject: [PATCH 34/63] fix imports --- src/c3nav/routing/api/positioning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index 31827c44..3e3bf460 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -8,9 +8,9 @@ from pydantic_extra_types.mac_address import MacAddress from c3nav.api.auth import auth_responses from c3nav.api.schema import BaseSchema -from c3nav.editor.models import ChangeSet from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.schemas.models import CustomLocationSchema +from c3nav.mapdata.tasks import update_ap_names_bssid_mapping from c3nav.mapdata.utils.cache.stats import increment_cache_key from c3nav.routing.locator import Locator from c3nav.routing.schemas import LocateWifiPeerSchema, LocateIBeaconPeerSchema @@ -41,7 +41,7 @@ class PositioningResult(BaseSchema): description="determine position based on wireless measurements " "(including ranging, if available)", response={200: PositioningResult, **auth_responses}) -def get_position(request, parameters: LocateRequestSchema, update_ap_names_bssid_mapping=None): +def get_position(request, parameters: LocateRequestSchema): try: location = Locator.load().locate(parameters.wifi_peers, permissions=AccessPermission.get_for_request(request)) From baf185228d2fab10f9cc3487d789f53d11a3ab0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 23:06:22 +0100 Subject: [PATCH 35/63] positioning for superusers pls? :pleasing_face_emoji: --- src/c3nav/routing/api/positioning.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index 3e3bf460..0dc573d2 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -59,7 +59,10 @@ def get_position(request, parameters: LocateRequestSchema): continue bssid_mapping.setdefault(peer.ap_name, set()).add(peer.bssid) if bssid_mapping: - update_ap_names_bssid_mapping.delay(map_name=bssid_mapping, user=request.user) + update_ap_names_bssid_mapping.delay( + map_name={name: list[bssids] for name, bssids in bssid_mapping.items()}, + user=request.user + ) return { "location": location From ece08fdd847825d8804e970472b50ca1aaa55419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 23:09:57 +0100 Subject: [PATCH 36/63] fix validation error --- src/c3nav/routing/api/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/routing/api/routing.py b/src/c3nav/routing/api/routing.py index 91075ed4..6d976a2c 100644 --- a/src/c3nav/routing/api/routing.py +++ b/src/c3nav/routing/api/routing.py @@ -140,7 +140,7 @@ class RouteLevelSchema(DjangoModelSchema): ) class RouteItemSchema(BaseSchema): - id: PositiveInt + id: Optional[PositiveInt] coordinates: Coordinates3D waytype: Union[ Annotated[ShortWayTypeSchema, APIField(title="waytype", descripiton="waytype used for this segment")], From 8a8958cf312b852c14132f7ef12c7dd113baeaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 23:17:20 +0100 Subject: [PATCH 37/63] blablablablabla --- src/c3nav/routing/api/positioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index 0dc573d2..32cf598a 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -60,7 +60,7 @@ def get_position(request, parameters: LocateRequestSchema): bssid_mapping.setdefault(peer.ap_name, set()).add(peer.bssid) if bssid_mapping: update_ap_names_bssid_mapping.delay( - map_name={name: list[bssids] for name, bssids in bssid_mapping.items()}, + map_name={str(name): [str(b) for b in bssids] for name, bssids in bssid_mapping.items()}, user=request.user ) From 980d9c0b3268cd98953cf9407e7a3b8e112a3a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 23:18:45 +0100 Subject: [PATCH 38/63] fucking fix it for us, pls --- src/c3nav/mapdata/tasks.py | 4 +++- src/c3nav/routing/api/positioning.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/c3nav/mapdata/tasks.py b/src/c3nav/mapdata/tasks.py index 8e1aa185..c2d600f8 100644 --- a/src/c3nav/mapdata/tasks.py +++ b/src/c3nav/mapdata/tasks.py @@ -2,6 +2,7 @@ import logging import time from celery.exceptions import MaxRetriesExceededError +from django.contrib.auth import get_user_model from django.core.cache import cache from django.utils.formats import date_format from django.utils.translation import gettext_lazy as _ @@ -57,7 +58,8 @@ def delete_map_cache_key(self, cache_key): @app.task(bind=True, max_retries=10) -def update_ap_names_bssid_mapping(self, map_name, user): +def update_ap_names_bssid_mapping(self, map_name, user_id): + user = get_user_model().objects.filter(pk=user_id) from c3nav.mapdata.models.geometry.space import RangingBeacon todo = [] for beacon in RangingBeacon.objects.filter(ap_name__in=map_name.keys(), diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index 32cf598a..c3157ba7 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -61,7 +61,7 @@ def get_position(request, parameters: LocateRequestSchema): if bssid_mapping: update_ap_names_bssid_mapping.delay( map_name={str(name): [str(b) for b in bssids] for name, bssids in bssid_mapping.items()}, - user=request.user + user_id=request.user.pk ) return { From 41d81582186b9f57e094c8e8dfe45d771ea2e2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 23:46:01 +0100 Subject: [PATCH 39/63] fix unknown spaces --- src/c3nav/routing/locator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 2c07d727..436a9b0c 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -235,21 +235,21 @@ class Locator: if not scan_data_we_can_use: return None + router = Router.load() + # get visible spaces best_ap_id = max(scan_data_we_can_use, key=lambda item: item[1].rssi)[0] space_id = self.peers[best_ap_id].space_id - space = self.spaces[space_id] + space = router.spaces[space_id] scan_data_in_the_same_room = sorted([ (peer_id, value) for peer_id, value in scan_data_we_can_use if self.peers[peer_id].space_id == space_id ], key=lambda a: -a[1].rssi) - router = Router.load() - if len(scan_data_in_the_same_room) == 1: - point = router.spaces[space.pk].point + point = space.point return CustomLocation( - level=router.levels[router.spaces[space.pk].level_id], + level=router.levels[space.level_id], x=point.x, y=point.y, permissions=permissions, @@ -264,7 +264,7 @@ class Locator: x += float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum y += float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum return CustomLocation( - level=router.levels[router.spaces[space.pk].level_id], + level=router.levels[space.level_id], x=x/100, y=y/100, permissions=permissions, From 73c0cbc0d2bef549e7dd71cf453bd76aab769bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 23:48:51 +0100 Subject: [PATCH 40/63] fix the thing with the sum? --- src/c3nav/routing/locator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 436a9b0c..63b97d7b 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -246,7 +246,9 @@ class Locator: (peer_id, value) for peer_id, value in scan_data_we_can_use if self.peers[peer_id].space_id == space_id ], key=lambda a: -a[1].rssi) - if len(scan_data_in_the_same_room) == 1: + the_sum = sum((value.rssi + 90) for peer_id, value in scan_data_in_the_same_room[:3]) + + if len(scan_data_in_the_same_room) == 1 or not the_sum: point = space.point return CustomLocation( level=router.levels[space.level_id], @@ -256,8 +258,6 @@ class Locator: icon='my_location' ) else: - the_sum = sum((value.rssi+90) for peer_id, value in scan_data_in_the_same_room[:3]) - x = 0 y = 0 for peer_id, value in scan_data_in_the_same_room[:3]: From bcc5d8d2f3b165696f96bdb78b504f9070697378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sat, 28 Dec 2024 23:51:32 +0100 Subject: [PATCH 41/63] rounded_pk --- src/c3nav/mapdata/utils/locations.py | 4 ++++ src/c3nav/routing/api/positioning.py | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/c3nav/mapdata/utils/locations.py b/src/c3nav/mapdata/utils/locations.py index 596bd515..88c557ce 100644 --- a/src/c3nav/mapdata/utils/locations.py +++ b/src/c3nav/mapdata/utils/locations.py @@ -293,6 +293,10 @@ class CustomLocation: y = round(self.y, 2) self.pk = 'c:%s:%s:%s' % (self.level.level_index, x, y) + @property + def rounded_pk(self): + return 'c:%s:%s:%s' % (self.level.level_index, self.x//5*5, self.y//5*5) + @property def serialized_geometry(self): return { diff --git a/src/c3nav/routing/api/positioning.py b/src/c3nav/routing/api/positioning.py index c3157ba7..85db46b5 100644 --- a/src/c3nav/routing/api/positioning.py +++ b/src/c3nav/routing/api/positioning.py @@ -46,8 +46,7 @@ def get_position(request, parameters: LocateRequestSchema): location = Locator.load().locate(parameters.wifi_peers, permissions=AccessPermission.get_for_request(request)) if location is not None: - # todo: this will overload us probably, group these - increment_cache_key('apistats__locate__%s' % location.pk) + increment_cache_key('apistats__locate__%s' % location.rounded_pk) except ValidationError: # todo: validation error, seriously? this shouldn't happen anyways raise From 1f70c4c1c5bb6075c4a4fde8b60126fd70744799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 00:39:03 +0100 Subject: [PATCH 42/63] hopefully this helps? --- src/c3nav/routing/locator.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 63b97d7b..2eb32d35 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -246,9 +246,17 @@ class Locator: (peer_id, value) for peer_id, value in scan_data_we_can_use if self.peers[peer_id].space_id == space_id ], key=lambda a: -a[1].rssi) + deduplicized_scan_data_in_the_same_room = [] + already_got = set() + for peer_id, value in scan_data_in_the_same_room: + key = tuple(self.peers[peer_id].xyz) + if key in already_got: + continue + deduplicized_scan_data_in_the_same_room.append((peer_id, value)) + the_sum = sum((value.rssi + 90) for peer_id, value in scan_data_in_the_same_room[:3]) - if len(scan_data_in_the_same_room) == 1 or not the_sum: + if not the_sum: point = space.point return CustomLocation( level=router.levels[space.level_id], From b42e0496e2eb0ce76e7ea189412954c83ab81a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 00:40:10 +0100 Subject: [PATCH 43/63] fix this --- src/c3nav/mapdata/tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/tasks.py b/src/c3nav/mapdata/tasks.py index c2d600f8..2897c29a 100644 --- a/src/c3nav/mapdata/tasks.py +++ b/src/c3nav/mapdata/tasks.py @@ -59,7 +59,9 @@ def delete_map_cache_key(self, cache_key): @app.task(bind=True, max_retries=10) def update_ap_names_bssid_mapping(self, map_name, user_id): - user = get_user_model().objects.filter(pk=user_id) + user = get_user_model().objects.filter(pk=user_id).first() + if user is None: + return from c3nav.mapdata.models.geometry.space import RangingBeacon todo = [] for beacon in RangingBeacon.objects.filter(ap_name__in=map_name.keys(), From c7fbcaf50f1bc91b77c638fbda3b99a77275dfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 01:02:56 +0100 Subject: [PATCH 44/63] don't have positioning results in levels_on_top --- src/c3nav/routing/locator.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 2eb32d35..c9fbd1f3 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -256,10 +256,13 @@ class Locator: the_sum = sum((value.rssi + 90) for peer_id, value in scan_data_in_the_same_room[:3]) + level = router.levels[space.level_id] + if level.on_top_of_id: + level = router.levels[level.on_top_of_id] if not the_sum: point = space.point return CustomLocation( - level=router.levels[space.level_id], + level=level, x=point.x, y=point.y, permissions=permissions, @@ -272,7 +275,7 @@ class Locator: x += float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum y += float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum return CustomLocation( - level=router.levels[space.level_id], + level=level, x=x/100, y=y/100, permissions=permissions, @@ -384,11 +387,16 @@ class Locator: restrictions = router.get_restrictions(permissions) result_pos = results.x + + level = router.levels[router.level_id_for_xyz( + (result_pos[0], result_pos[1], result_pos[2] - 1.3), # -1.3m cause we assume people to be above ground + restrictions + )] + if level.on_top_of_id: + level = router.levels[level.on_top_of_id] + location = CustomLocation( - level=router.levels[router.level_id_for_xyz( - (result_pos[0], result_pos[1], result_pos[2]-1.3), # -1.3m cause we assume people to be above ground - restrictions - )], + level=level, x=result_pos[0]/100, y=result_pos[1]/100, permissions=permissions, From 8e79a937ce7d730f3482f083e6e7ef4dc925a63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 01:07:59 +0100 Subject: [PATCH 45/63] use deduplicized data --- src/c3nav/routing/locator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index c9fbd1f3..878a92cb 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -254,7 +254,7 @@ class Locator: continue deduplicized_scan_data_in_the_same_room.append((peer_id, value)) - the_sum = sum((value.rssi + 90) for peer_id, value in scan_data_in_the_same_room[:3]) + the_sum = sum((value.rssi + 90) for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]) level = router.levels[space.level_id] if level.on_top_of_id: @@ -271,7 +271,7 @@ class Locator: else: x = 0 y = 0 - for peer_id, value in scan_data_in_the_same_room[:3]: + for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]: x += float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum y += float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum return CustomLocation( From 5396469a6e56b74bef5e72d3cac188faf7f95f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 01:49:25 +0100 Subject: [PATCH 46/63] comment out locate_range for now --- src/c3nav/routing/locator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 878a92cb..90436b96 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -217,9 +217,9 @@ class Locator: if not scan_data: return None - result = self.locate_range(scan_data, permissions) - if result is not None: - return result + #result = self.locate_range(scan_data, permissions) + #if result is not None: + # return result result = self.locate_by_beacon_positions(scan_data, permissions) if result is not None: From 2325fe6a4d3383718663fd3117ab69ca771ccf74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 01:54:03 +0100 Subject: [PATCH 47/63] Revert "comment out locate_range for now" This reverts commit 5396469a6e56b74bef5e72d3cac188faf7f95f95. --- src/c3nav/routing/locator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 90436b96..878a92cb 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -217,9 +217,9 @@ class Locator: if not scan_data: return None - #result = self.locate_range(scan_data, permissions) - #if result is not None: - # return result + result = self.locate_range(scan_data, permissions) + if result is not None: + return result result = self.locate_by_beacon_positions(scan_data, permissions) if result is not None: From 5a32942fec9836ab486e8fced3ee5667f930df53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 02:09:18 +0100 Subject: [PATCH 48/63] add ap_name identifier --- src/c3nav/routing/locator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 878a92cb..58100402 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -96,6 +96,8 @@ class Locator: identifiers = [] for bssid in beacon.addresses: identifiers.append(TypedIdentifier(PeerType.WIFI, bssid)) + if beacon.ap_name: + identifiers.append(TypedIdentifier(PeerType.WIFI, beacon.ap_name)) if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None: identifiers.append( TypedIdentifier(PeerType.IBEACON, (beacon.ibeacon_uuid, beacon.ibeacon_major, beacon.ibeacon_minor)) From 0b88260c7c7db3a8fafb542d69323ae5c858c340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 02:12:55 +0100 Subject: [PATCH 49/63] some more locator bug fixes --- src/c3nav/routing/locator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 58100402..26a00945 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -95,7 +95,7 @@ class Locator: for beacon in calculated.beacons.values(): identifiers = [] for bssid in beacon.addresses: - identifiers.append(TypedIdentifier(PeerType.WIFI, bssid)) + identifiers.append(TypedIdentifier(PeerType.WIFI, bssid.lower())) if beacon.ap_name: identifiers.append(TypedIdentifier(PeerType.WIFI, beacon.ap_name)) if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None: @@ -142,7 +142,7 @@ class Locator: if settings.WIFI_SSIDS and scan_value.ssid not in settings.WIFI_SSIDS: continue peer_ids = { - self.get_peer_id(TypedIdentifier(PeerType.WIFI, scan_value.bssid), create=create_peers), + self.get_peer_id(TypedIdentifier(PeerType.WIFI, scan_value.bssid.lower()), create=create_peers), self.get_peer_id(TypedIdentifier(PeerType.WIFI, scan_value.ap_name), create=create_peers), } - {None, ""} for peer_id in peer_ids: @@ -254,6 +254,7 @@ class Locator: key = tuple(self.peers[peer_id].xyz) if key in already_got: continue + already_got.add(key) deduplicized_scan_data_in_the_same_room.append((peer_id, value)) the_sum = sum((value.rssi + 90) for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]) From 7f60e652777a802ade01fb7f7cc1494a2dbe5d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 02:16:45 +0100 Subject: [PATCH 50/63] more improvements with locate maybe --- src/c3nav/routing/locator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 26a00945..bb0e00ec 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -257,7 +257,7 @@ class Locator: already_got.add(key) deduplicized_scan_data_in_the_same_room.append((peer_id, value)) - the_sum = sum((value.rssi + 90) for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]) + the_sum = sum((value.rssi + 100) for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]) level = router.levels[space.level_id] if level.on_top_of_id: @@ -274,9 +274,10 @@ class Locator: else: x = 0 y = 0 + # sure this can be better probably for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]: - x += float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum - y += float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum + x += float(self.peers[peer_id].xyz[0]) * (value.rssi+100) / the_sum + y += float(self.peers[peer_id].xyz[1]) * (value.rssi+100) / the_sum return CustomLocation( level=level, x=x/100, From 3190f950fc9195cb786e3015d0b1603fead7045d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 02:18:29 +0100 Subject: [PATCH 51/63] =?UTF-8?q?filter=20out=20rssi=20-1=E2=80=A6=20maybe?= =?UTF-8?q?=20we=20might=20be=20sending=20that=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/c3nav/routing/locator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index bb0e00ec..1302eed7 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -231,7 +231,8 @@ class Locator: def locate_by_beacon_positions(self, scan_data: ScanData, permissions=None): scan_data_we_can_use = [ - (peer_id, value) for peer_id, value in scan_data.items() if self.peers[peer_id].space_id + (peer_id, value) for peer_id, value in scan_data.items() + if self.peers[peer_id].space_id and -90 < value.rssi < -10 ] if not scan_data_we_can_use: @@ -257,7 +258,7 @@ class Locator: already_got.add(key) deduplicized_scan_data_in_the_same_room.append((peer_id, value)) - the_sum = sum((value.rssi + 100) for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]) + the_sum = sum((value.rssi + 90) for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]) level = router.levels[space.level_id] if level.on_top_of_id: @@ -276,8 +277,8 @@ class Locator: y = 0 # sure this can be better probably for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]: - x += float(self.peers[peer_id].xyz[0]) * (value.rssi+100) / the_sum - y += float(self.peers[peer_id].xyz[1]) * (value.rssi+100) / the_sum + x += float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum + y += float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum return CustomLocation( level=level, x=x/100, From 652223085b89c1707969311e13a3d9b6f03f2038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 15:21:31 +0100 Subject: [PATCH 52/63] fix redirect slugs none behavior (BUT THIS SHOULDN'T HAPPEN) --- src/c3nav/editor/forms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 1fddd69d..8deafd06 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -262,7 +262,8 @@ class EditorFormBase(I18nModelFormMixin, ModelForm): self.add_redirect_slugs = None self.remove_redirect_slugs = None if 'slug' in self.fields: - self.redirect_slugs = (sorted(self.instance.redirects.values_list('slug', flat=True)) + self.redirect_slugs = (sorted(slug for slug in self.instance.redirects.values_list('slug', flat=True) + if slug) # THIS SHOULD NEVER BE NONE if self.instance.pk else []) self.fields['redirect_slugs'] = CharField(label=_('Redirecting Slugs (comma separated)'), required=False, initial=','.join(self.redirect_slugs)) From 8112abad314dae12d9848ce8092fdb26e470eb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 15:50:13 +0100 Subject: [PATCH 53/63] improve locating a bit by using the pointplacementhelper --- .../mapdata/management/commands/importnoc.py | 4 +- .../mapdata/management/commands/importpoc.py | 4 +- .../utils/{importer.py => placement.py} | 43 +++++++++++++------ src/c3nav/routing/locator.py | 40 ++++++++++------- 4 files changed, 57 insertions(+), 34 deletions(-) rename src/c3nav/mapdata/utils/{importer.py => placement.py} (59%) diff --git a/src/c3nav/mapdata/management/commands/importnoc.py b/src/c3nav/mapdata/management/commands/importnoc.py index 8535ceaf..2fbdfb4f 100644 --- a/src/c3nav/mapdata/management/commands/importnoc.py +++ b/src/c3nav/mapdata/management/commands/importnoc.py @@ -6,7 +6,7 @@ from shapely import distance from c3nav.mapdata.models import MapUpdate from c3nav.mapdata.models.geometry.space import RangingBeacon -from c3nav.mapdata.utils.importer import PointImportHelper +from c3nav.mapdata.utils.placement import PointPlacementHelper from c3nav.mapdata.utils.cache.changes import changed_geometries from c3nav.mapdata.utils.geometry import unwrap_geom @@ -37,7 +37,7 @@ class Command(BaseCommand): MapUpdate.objects.create(type='importnoc') def do_import(self, items: dict[str, NocImportItem]): - import_helper = PointImportHelper() + import_helper = PointPlacementHelper() beacons_so_far: dict[str, RangingBeacon] = { **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="noc:", diff --git a/src/c3nav/mapdata/management/commands/importpoc.py b/src/c3nav/mapdata/management/commands/importpoc.py index f0522db1..554c4527 100644 --- a/src/c3nav/mapdata/management/commands/importpoc.py +++ b/src/c3nav/mapdata/management/commands/importpoc.py @@ -14,7 +14,7 @@ from c3nav.mapdata.models import MapUpdate, Level from c3nav.mapdata.models.geometry.space import RangingBeacon from c3nav.mapdata.utils.cache.changes import changed_geometries from c3nav.mapdata.utils.geometry import unwrap_geom -from c3nav.mapdata.utils.importer import PointImportHelper +from c3nav.mapdata.utils.placement import PointPlacementHelper class PocImportItemProperties(BaseModel): @@ -46,7 +46,7 @@ class Command(BaseCommand): MapUpdate.objects.create(type='importnoc') def do_import(self, items: list[PocImportItem]): - import_helper = PointImportHelper() + import_helper = PointPlacementHelper() beacons_so_far: dict[str, RangingBeacon] = { **{m.import_tag: m for m in RangingBeacon.objects.filter(import_tag__startswith="poc:", diff --git a/src/c3nav/mapdata/utils/importer.py b/src/c3nav/mapdata/utils/placement.py similarity index 59% rename from src/c3nav/mapdata/utils/importer.py rename to src/c3nav/mapdata/utils/placement.py index 79481b70..e0c498ea 100644 --- a/src/c3nav/mapdata/utils/importer.py +++ b/src/c3nav/mapdata/utils/placement.py @@ -1,11 +1,14 @@ +from typing import Optional + from shapely import Point, distance from shapely.ops import unary_union, nearest_points from c3nav.mapdata.models import Level, Space from c3nav.mapdata.utils.geometry import unwrap_geom +from c3nav.routing.router import RouterRestrictionSet -class PointImportHelper: +class PointPlacementHelper: def __init__(self): self.spaces_for_level = {} self.levels = tuple(Level.objects.values_list("pk", flat=True)) @@ -14,23 +17,30 @@ class PointImportHelper: for space in Space.objects.select_related('level').prefetch_related('holes'): self.spaces_for_level.setdefault(space.level_id, []).append(space) - def get_point_and_space(self, level_id: int, point: Point, name: str): + def get_point_and_space(self, level_id: int, point: Point, name: Optional[str] = None, + restrictions: Optional[RouterRestrictionSet] = None, max_space_distance=1.5): # determine space + restricted_spaces = restrictions.spaces if restrictions else () possible_spaces = [space for space in self.spaces_for_level[level_id] - if space.geometry.intersects(point)] + if space.pk not in restricted_spaces and space.geometry.intersects(point)] + if not possible_spaces: possible_spaces = [space for space in self.spaces_for_level[level_id] - if distance(unwrap_geom(space.geometry), point) < 1.5] + if (space.pk not in restricted_spaces + and distance(unwrap_geom(space.geometry), point) < max_space_distance)] if len(possible_spaces) == 1: new_space = possible_spaces[0] the_distance = distance(unwrap_geom(new_space.geometry), point) - print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}") + if name: + print(f"SUCCESS: {name} is {the_distance:.02f}m away from {new_space.title}") elif len(possible_spaces) > 1: new_space = min(possible_spaces, key=lambda s: distance(unwrap_geom(s.geometry), point)) - print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}, " - f"which is {distance(unwrap_geom(new_space.geometry), point)}m away...") + if name: + print(f"WARNING: {name} could be in multiple spaces ({possible_spaces}, picking {new_space}, " + f"which is {distance(unwrap_geom(new_space.geometry), point)}m away...") else: - print(f"ERROR: {name} is not within any space on level {level_id} ({point})") + if name: + print(f"ERROR: {name} is not within any space on level {level_id} ({point})") return None, None # move point into space if needed @@ -41,9 +51,11 @@ class PointImportHelper: point = nearest_points(new_space_geometry.buffer(-0.05), point)[0] elif len(possible_spaces) == 1: new_space = possible_spaces[0] - print(f"SUCCESS: {name} is in {new_space.title}") + if name: + print(f"SUCCESS: {name} is in {new_space.title}") else: - print(f"WARNING: {name} could be in multiple spaces, picking one...") + if name: + print(f"WARNING: {name} could be in multiple spaces, picking one...") new_space = possible_spaces[0] lower_levels = self.lower_levels_for_level[new_space.level_id] @@ -52,15 +64,18 @@ class PointImportHelper: if not unary_union([unwrap_geom(h.geometry) for h in new_space.holes.all()]).intersects(point): # current selected spacae is fine, that's it break - print(f"NOTE: {name} is in a hole, looking lower...") + if name: + print(f"NOTE: {name} is in a hole, looking lower...") # find a lower space possible_spaces = [space for space in self.spaces_for_level[lower_level] - if space.geometry.intersects(point)] + if space.pk not in restricted_spaces and space.geometry.intersects(point)] if possible_spaces: new_space = possible_spaces[0] - print(f"NOTE: {name} moved to lower space {new_space}") + if name: + print(f"NOTE: {name} moved to lower space {new_space}") else: - print(f"WARNING: {name} couldn't find a lower space, still in a hole") + if name: + print(f"WARNING: {name} couldn't find a lower space, still in a hole") return new_space, point diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 1302eed7..4cf85558 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -12,9 +12,11 @@ from annotated_types import Lt from django.conf import settings from pydantic.types import NonNegativeInt from pydantic_extra_types.mac_address import MacAddress +from shapely import Point from c3nav.mapdata.models import MapUpdate, Space from c3nav.mapdata.utils.locations import CustomLocation +from c3nav.mapdata.utils.placement import PointPlacementHelper from c3nav.mesh.utils import get_nodes_and_ranging_beacons from c3nav.routing.router import Router from c3nav.routing.schemas import LocateWifiPeerSchema, BeaconMeasurementDataSchema, LocateIBeaconPeerSchema @@ -82,6 +84,7 @@ class Locator: peer_lookup: dict[TypedIdentifier, int] = field(default_factory=dict) xyz: np.array = field(default_factory=(lambda: np.empty((0,)))) spaces: dict[int, "LocatorSpace"] = field(default_factory=dict) + placement_helper: Optional[PointPlacementHelper] = None @classmethod def rebuild(cls, update, router): @@ -127,6 +130,8 @@ class Locator: if new_space.points: self.spaces[space.pk] = new_space + self.placement_helper = PointPlacementHelper() + def get_peer_id(self, identifier: TypedIdentifier, create=False) -> Optional[int]: peer_id = self.peer_lookup.get(identifier, None) if peer_id is None and create: @@ -239,6 +244,7 @@ class Locator: return None router = Router.load() + restrictions = router.get_restrictions(permissions) # get visible spaces best_ap_id = max(scan_data_we_can_use, key=lambda item: item[1].rssi)[0] @@ -261,17 +267,8 @@ class Locator: the_sum = sum((value.rssi + 90) for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]) level = router.levels[space.level_id] - if level.on_top_of_id: - level = router.levels[level.on_top_of_id] if not the_sum: point = space.point - return CustomLocation( - level=level, - x=point.x, - y=point.y, - permissions=permissions, - icon='my_location' - ) else: x = 0 y = 0 @@ -279,13 +276,24 @@ class Locator: for peer_id, value in deduplicized_scan_data_in_the_same_room[:3]: x += float(self.peers[peer_id].xyz[0]) * (value.rssi+90) / the_sum y += float(self.peers[peer_id].xyz[1]) * (value.rssi+90) / the_sum - return CustomLocation( - level=level, - x=x/100, - y=y/100, - permissions=permissions, - icon='my_location' - ) + point = Point(x/100, y/100) + + new_space, new_point = self.placement_helper.get_point_and_space( + level_id=level.pk, point=point, restrictions=restrictions, + max_space_distance=20, + ) + + level = router.levels[new_space.level_id] + if level.on_top_of_id: + level = router.levels[level.on_top_of_id] + + return CustomLocation( + level=level, + x=x / 100, + y=y / 100, + permissions=permissions, + icon='my_location' + ) def locate_rssi(self, scan_data: ScanData, permissions=None): router = Router.load() From 2f2eef4ffcf8c64ca46f819499bd6a312c3b6c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 16:14:55 +0100 Subject: [PATCH 54/63] fix update.timeout --- src/c3nav/mapdata/api/map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index da30b832..40c50850 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -332,7 +332,7 @@ def set_position(request, position_id: AnyPositionID, update: UpdatePositionSche raise APIRequestValidationFailed('Cant resolve coordinates.') location.coordinates_id = update.coordinates_id - location.timeout = update.timeout + location.timeout = update.timeout or 0 location.last_coordinates_update = timezone.now() location.save() From 431bbbd1f639f9ac7b542a38b23f0c2c898dae9e Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Sun, 29 Dec 2024 15:50:22 +0100 Subject: [PATCH 55/63] put nearby into its own layers so that it isn't clustered together with the active location marker --- src/c3nav/site/static/site/js/c3nav.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index efd338b4..88d7133e 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1749,6 +1749,7 @@ c3nav = { // setup level control c3nav._levelControl = new LevelControl({initialTheme: c3nav.theme}).addTo(c3nav.map); c3nav._locationLayers = {}; + c3nav._nearbyLayers = {}; c3nav._locationLayerBounds = {}; c3nav._detailLayers = {}; c3nav._routeLayers = {}; @@ -1763,7 +1764,8 @@ c3nav = { const level = c3nav.levels[i]; const layerGroup = c3nav._levelControl.addLevel(level[0], level[2]); c3nav._detailLayers[level[0]] = L.layerGroup().addTo(layerGroup); - c3nav._locationLayers[level[0]] = L.markerClusterGroup({ + c3nav._locationLayers[level[0]] = L.layerGroup().addTo(layerGroup); + c3nav._nearbyLayers[level[0]] = L.markerClusterGroup({ maxClusterRadius: 35, spiderLegPolylineOptions: { color: '#4b6c97', @@ -1996,6 +1998,9 @@ c3nav = { for (const level_id in c3nav._locationLayers) { c3nav._locationLayers[level_id].clearLayers() } + for (const level_id in c3nav._nearbyLayers) { + c3nav._nearbyLayers[level_id].clearLayers() + } c3nav._visible_map_locations = []; if (origin) c3nav._merge_bounds(bounds, c3nav._add_location_to_map(origin, single ? c3nav.icons.default : c3nav.icons.origin)); if (destination) c3nav._merge_bounds(bounds, c3nav._add_location_to_map(destination, single ? c3nav.icons.default : c3nav.icons.destination)); @@ -2003,20 +2008,21 @@ c3nav = { if (c3nav.state.nearby && destination && 'areas' in destination) { if (destination.space) { done.add(destination.space); - c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[destination.space], c3nav.icons.nearby, true)); + c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[destination.space], c3nav.icons.nearby, true, c3nav._nearbyLayers)); } if (destination.near_area) { done.add(destination.near_area); - c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[destination.near_area], c3nav.icons.nearby, true)); + c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[destination.near_area], c3nav.icons.nearby, true, c3nav._nearbyLayers)); } for (const area of destination.areas) { if (done.has(area)) continue; done.add(area); - c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[area], c3nav.icons.nearby, true)); + c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[area], c3nav.icons.nearby, true, c3nav._nearbyLayers)); } for (const location of destination.nearby) { if (done.has(location)) continue; - c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[location], c3nav.icons.nearby, true)); + done.add(destination.nearby); + c3nav._merge_bounds(bounds, c3nav._add_location_to_map(c3nav.locations_by_id[location], c3nav.icons.nearby, true, c3nav._nearbyLayers)); } } c3nav._locationLayerBounds = bounds; @@ -2081,7 +2087,10 @@ c3nav = { ]; }, _location_point_overrides: {}, - _add_location_to_map: function (location, icon, no_geometry) { + _add_location_to_map: function (location, icon, no_geometry, layers) { + if (!layers) { + layers = c3nav._locationLayers; + } if (!location) { // if location is not in the searchable list... return @@ -2124,7 +2133,7 @@ c3nav = { }).bindPopup(location.elem + buttons_html, c3nav._add_map_padding({ className: 'location-popup', maxWidth: 500 - }, 'autoPanPaddingTopLeft', 'autoPanPaddingBottomRight')).addTo(c3nav._locationLayers[location.point[0]]); + }, 'autoPanPaddingTopLeft', 'autoPanPaddingBottomRight')).addTo(layers[location.point[0]]); const result = {}; result[location.point[0]] = L.latLngBounds( From fa0f8bc0442f2b8198ed3f115aeb0b928a4ad5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 16:36:45 +0100 Subject: [PATCH 56/63] fix CustomLocation.serialize_popsition() --- src/c3nav/mapdata/models/locations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index d109c082..69afa133 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -23,6 +23,7 @@ from c3nav.mapdata.fields import I18nField from c3nav.mapdata.grid import grid from c3nav.mapdata.models.access import AccessRestrictionMixin from c3nav.mapdata.models.base import SerializableMixin, TitledMixin +from c3nav.mapdata.schemas.models import CustomLocationLocationSchema from c3nav.mapdata.utils.cache.local import per_request_cache from c3nav.mapdata.utils.fields import LocationById from c3nav.mapdata.utils.models import get_submodels @@ -640,7 +641,8 @@ class Position(CustomLocationProxyMixin, models.Model): 'subtitle': _('currently unavailable'), } from c3nav.mapdata.schemas.models import CustomLocationSchema - result = CustomLocationSchema.model_validate(custom_location).model_dump() + # todo: is this good? + result = CustomLocationLocationSchema.model_validate(custom_location).model_dump() result.update({ 'available': True, 'id': 'm:%s' % self.secret, From d7a41e1c585ffd9c67014d0c808b8a3e0118fd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 16:41:36 +0100 Subject: [PATCH 57/63] fix last thing --- src/c3nav/mapdata/models/locations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 69afa133..b2995223 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -23,7 +23,6 @@ from c3nav.mapdata.fields import I18nField from c3nav.mapdata.grid import grid from c3nav.mapdata.models.access import AccessRestrictionMixin from c3nav.mapdata.models.base import SerializableMixin, TitledMixin -from c3nav.mapdata.schemas.models import CustomLocationLocationSchema from c3nav.mapdata.utils.cache.local import per_request_cache from c3nav.mapdata.utils.fields import LocationById from c3nav.mapdata.utils.models import get_submodels @@ -640,8 +639,8 @@ class Position(CustomLocationProxyMixin, models.Model): 'title': self.name, 'subtitle': _('currently unavailable'), } - from c3nav.mapdata.schemas.models import CustomLocationSchema # todo: is this good? + from c3nav.mapdata.schemas.models import CustomLocationLocationSchema result = CustomLocationLocationSchema.model_validate(custom_location).model_dump() result.update({ 'available': True, From 1888c91f3fc210e5a3e3570828b19fa310df791b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 16:41:42 +0100 Subject: [PATCH 58/63] don#t fail if user is in an unknown space --- src/c3nav/routing/locator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 4cf85558..2cff0528 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -283,14 +283,15 @@ class Locator: max_space_distance=20, ) - level = router.levels[new_space.level_id] + if new_space is not None: + level = router.levels[new_space.level_id] if level.on_top_of_id: level = router.levels[level.on_top_of_id] return CustomLocation( level=level, - x=x / 100, - y=y / 100, + x=point.x, + y=point.y, permissions=permissions, icon='my_location' ) From 363b6b007522e43b7801d0bedfa733a12f62e4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 16:43:59 +0100 Subject: [PATCH 59/63] no restrictions for get_point_and_space in locator --- src/c3nav/routing/locator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py index 2cff0528..9a95f422 100644 --- a/src/c3nav/routing/locator.py +++ b/src/c3nav/routing/locator.py @@ -279,7 +279,7 @@ class Locator: point = Point(x/100, y/100) new_space, new_point = self.placement_helper.get_point_and_space( - level_id=level.pk, point=point, restrictions=restrictions, + level_id=level.pk, point=point, max_space_distance=20, ) From 94dc9af7e6385951d90f168b3b8841c84d6d739f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 17:11:30 +0100 Subject: [PATCH 60/63] detect positions well --- src/c3nav/site/static/site/js/c3nav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 88d7133e..4e4a0ce3 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -2095,7 +2095,7 @@ c3nav = { // if location is not in the searchable list... return } - if (location.dynamic || location.locationtype === "dynamiclocation") { + if (location.dynamic || location.locationtype === "dynamiclocation" || location.locationtype === "position") { if (!('available' in location)) { c3nav_api.get(`map/positions/${location.id}/`) .then(c3nav._dynamic_location_loaded); From 12a0db43e73c00fba5aff50784938319b8d20b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 17:32:25 +0100 Subject: [PATCH 61/63] add effective_slug to TrackablePositionSchema --- src/c3nav/mapdata/schemas/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index ee9e87d1..ddc54116 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -601,6 +601,10 @@ class TrackablePositionSchema(BaseSchema): description="slug representing the position", example="p:adskjfalskdj", ) + effective_slug: PositionID = APIField( + description="slug representing the position", + example="p:adskjfalskdj", + ) icon: Optional[NonEmptyStr] = APIField( # todo: not optional? title="set icon name", description="icon as set in the location specifically (any material design icon name)", From ec61155833d7afcae242bd09bd54f10ac8c345dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 17:36:07 +0100 Subject: [PATCH 62/63] get all of your own positions --- src/c3nav/mapdata/api/map.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index 40c50850..da993ae2 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -295,6 +295,18 @@ def get_position_by_id(request, position_id: AnyPositionID): return location.serialize_position(request=request) +@map_api_router.get('/positions/my/', summary="all moving position coordinates", + description="get current coordinates of all moving positions owned be the current users", + response={200: list[AnyPositionStatusSchema], **API404.dict(), **auth_responses}) +@api_stats('get_position') +def get_my_positions(request, position_id: AnyPositionID): + # no caching for obvious reasons! + return [ + position.serialize_position(request=request) + for position in Position.objects.filter(owner=request.user) + ] + + class UpdatePositionSchema(BaseSchema): coordinates_id: Union[ Annotated[CustomLocationID, APIField(title="set coordinates")], From fb29533a60375aeb792d4718fc8ca14cc7c25034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 29 Dec 2024 17:41:47 +0100 Subject: [PATCH 63/63] add Position.short_name --- .../migrations/0137_position_short_name.py | 31 +++++++++++++++++++ src/c3nav/mapdata/models/locations.py | 1 + src/c3nav/site/forms.py | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/c3nav/mapdata/migrations/0137_position_short_name.py diff --git a/src/c3nav/mapdata/migrations/0137_position_short_name.py b/src/c3nav/mapdata/migrations/0137_position_short_name.py new file mode 100644 index 00000000..faf062a7 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0137_position_short_name.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.8 on 2024-12-29 16:38 + +from django.db import migrations, models + + +def generate_short_name(apps, schema_editor): + Position = apps.get_model('mapdata', 'position') + for position in Position.objects.all(): + position.short_name = position.name[:2] + position.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0136_wifi_bssids_to_addresses_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='position', + name='short_name', + field=models.CharField(help_text='two characters maximum', max_length=2, null=True, verbose_name='abbreviation'), + ), + migrations.RunPython(generate_short_name, migrations.RunPython.noop), + migrations.AlterField( + model_name='position', + name='short_name', + field=models.CharField(help_text='two characters maximum', max_length=2, verbose_name='abbreviation'), + ), + ] diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index b2995223..8d8d79dd 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -587,6 +587,7 @@ class Position(CustomLocationProxyMixin, models.Model): owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) name = models.CharField(_('name'), max_length=32) + short_name = models.CharField(_('abbreviation'), help_text=_('two characters maximum'), max_length=2) secret = models.CharField(_('secret'), unique=True, max_length=32, default=get_position_secret) last_coordinates_update = models.DateTimeField(_('last coordinates update'), null=True) timeout = models.PositiveSmallIntegerField(_('timeout (in seconds)'), default=0, blank=True, diff --git a/src/c3nav/site/forms.py b/src/c3nav/site/forms.py index fc51eeb2..1f98071f 100644 --- a/src/c3nav/site/forms.py +++ b/src/c3nav/site/forms.py @@ -80,7 +80,7 @@ class ReportUpdateForm(ModelForm): class PositionForm(ModelForm): class Meta: model = Position - fields = ['name', 'timeout'] + fields = ['name' ,"short_name", 'timeout'] class PositionSetForm(Form):