From 06203e6ccae1f6c2de7929ccf1a4b570b893d28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Mon, 25 Dec 2017 16:41:59 +0100 Subject: [PATCH] introducing the Locator --- src/c3nav/editor/forms.py | 28 +----- src/c3nav/mapdata/models/update.py | 4 + src/c3nav/routing/locator.py | 135 +++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 src/c3nav/routing/locator.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 20f8d967..63f25fcf 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -1,6 +1,5 @@ import json import operator -import re from functools import reduce from itertools import chain @@ -170,31 +169,10 @@ class EditorFormBase(I18nModelFormMixin, ModelForm): data = json.loads(self.cleaned_data['data']) except json.JSONDecodeError: raise ValidationError(_('Invalid JSON.')) - invalid_scan = ValidationError(_('Invalid Scan.')) - if not isinstance(data, list): - raise invalid_scan - if not data: - raise ValidationError(_('Needs to be one scan at minimum.')) - needed_keys = set(('bssid', 'ssid', 'level', 'frequency')) - allowed_keys = needed_keys | set(('last', )) - for scan in data: - if not isinstance(data, list): - raise invalid_scan - for ap in scan: - if not isinstance(ap, dict): - raise invalid_scan - keys = set(ap.keys()) - if (keys - allowed_keys) or (needed_keys - keys): - raise invalid_scan - if not re.match(r'^([0-9A-F]{2}:){5}[0-9A-F]{2}$', ap['bssid']): - raise invalid_scan - if not isinstance(ap['level'], int) or not (-1 >= ap['level'] >= -100): - raise invalid_scan - if not isinstance(ap['frequency'], int) or not (6000 > ap['frequency'] > 1000): - raise invalid_scan - if 'last' in keys and (not isinstance(ap['last'], int) or ap['last'] <= 0): - raise invalid_scan + from c3nav.routing.locator import LocatorPoint + LocatorPoint.validate_scans(data) + return data def clean(self): diff --git a/src/c3nav/mapdata/models/update.py b/src/c3nav/mapdata/models/update.py index e5590a98..5c3f2d88 100644 --- a/src/c3nav/mapdata/models/update.py +++ b/src/c3nav/mapdata/models/update.py @@ -158,6 +158,10 @@ class MapUpdate(models.Model): from c3nav.routing.router import Router Router.rebuild() + logger.info('Rebuilding locator...') + from c3nav.routing.locator import Locator + Locator.rebuild() + for new_update in new_updates: new_update.processed = True new_update.save() diff --git a/src/c3nav/routing/locator.py b/src/c3nav/routing/locator.py new file mode 100644 index 00000000..a57c42b6 --- /dev/null +++ b/src/c3nav/routing/locator.py @@ -0,0 +1,135 @@ +import operator +import os +import pickle +import re +from collections import deque, namedtuple +from functools import reduce + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from c3nav.mapdata.models import Space + + +class Locator: + filename = os.path.join(settings.CACHE_ROOT, 'locator') + + def __init__(self, stations, spaces): + self.stations = stations + self.spaces = spaces + + @classmethod + def rebuild(cls): + stations = LocatorStations() + spaces = {} + for space in Space.objects.prefetch_related('wifi_measurements'): + spaces[space.pk] = LocatorSpace( + LocatorPoint.from_measurement(measurement, stations) + for measurement in space.wifi_measurements.all() + ) + + locator = cls(stations, spaces) + pickle.dump(locator, open(cls.filename, 'wb')) + return locator + + +class LocatorStations: + def __init__(self): + self.stations = [] + self.stations_lookup = {} + + def get_or_create(self, bssid, frequency): + station_id = self.stations_lookup.get(bssid, None) + if station_id is not None: + station = self.stations[station_id] + station.frequencies.add(frequency) + else: + station = LocatorStation(bssid, set((frequency, ))) + self.stations_lookup[bssid] = len(self.stations) + self.stations.append(LocatorStation(bssid, set((frequency, )))) + return station + + +class LocatorSpace: + def __init__(self, points): + self.points = tuple(points) + self.stations_set = reduce(operator.or_, (frozenset(point.values.keys()) for point in points), frozenset()) + self.stations = tuple(self.stations_set) + self.stations_lookup = {station_id: i for i, station_id in enumerate(self.stations)} + + +class LocatorPoint(namedtuple('LocatorPoint', ('x', 'y', 'values'))): + @classmethod + def from_measurement(cls, measurement, stations: LocatorStations): + return cls(x=measurement.geometry.x, y=measurement.geometry.y, + values=cls.convert_scans(measurement.data, stations)) + + @classmethod + def convert_scan(cls, scan, stations: LocatorStations): + values = {} + for scan_value in scan: + station_id = stations.get_or_create(scan_value['bssid'], scan_value['frequency']) + # todo: convert to something more or less linear + values[station_id] = scan_value['level'] + + @classmethod + def convert_scans(cls, scans, stations: LocatorStations): + values_list = deque() + for scan in scans: + values_list.append(cls.convert_scan(scan, stations)) + + station_ids = reduce(operator.or_, (frozenset(values.keys()) for values in values_list), frozenset()) + return { + station_id: sum(values.get(station_id, -100) for values in values_list) / len(values_list) + for station_id in station_ids + } + + valid_frequencies = frozenset(( + 2412, 2417, 2422, 2427, 2432, 2437, 2442, 2447, 2452, 2457, 2462, 2467, 2472, 2484, + 5180, 5190, 5200, 5210, 5220, 5230, 5240, 5250, 5260, 5270, 5280, 5290, 5300, 5310, 5320, + 5500, 5510, 5520, 5530, 5540, 5550, 5560, 5570, 5580, 5590, 5600, 5610, 5620, 5630, 5640, + 5660, 5670, 5680, 5690, 5700, 5710, 5720, 5745, 5755, 5765, 5775, 5785, 5795, 5805, 5825 + )) + invalid_scan = ValidationError(_('Invalid Scan.')) + needed_keys = frozenset(('bssid', 'ssid', 'level', 'frequency')) + allowed_keys = needed_keys | frozenset(('last', )) + + @classmethod + def validate_scans(cls, data): + if not isinstance(data, list): + raise cls.invalid_scan + for scan in data: + cls.validate_scan(data) + + @classmethod + def validate_scan(cls, data): + if not isinstance(data, list): + raise cls.invalid_scan + for scan_value in data: + cls.validate_scan_value(scan_value) + + @classmethod + def validate_scan_value(cls, data): + if not isinstance(data, dict): + raise cls.invalid_scan + keys = frozenset(data.keys()) + if (keys - cls.allowed_keys) or (cls.needed_keys - keys): + raise cls.invalid_scan + if not re.match(r'^([0-9A-F]{2}:){5}[0-9A-F]{2}$', data['bssid']): + raise cls.invalid_scan + if not isinstance(data['level'], int) or not (-1 >= data['level'] >= -100): + raise cls.invalid_scan + if data['frequency'] not in cls.valid_frequencies: + raise cls.invalid_scan + if 'last' in keys and (not isinstance(data['last'], int) or data['last'] <= 0): + raise cls.invalid_scan + + +class LocatorStation: + def __init__(self, bssid, frequencies=()): + self.bssid = bssid + self.frequencies = set(frequencies) + + def __repr__(self): + return 'LocatorStation(%r, frequencies=%r)' % (self.bssid, self.frequencies)