first implementation of wifi-based location

This commit is contained in:
Laura Klünder 2017-12-25 23:56:12 +01:00
parent 03018a2f46
commit 580c4f0b85
3 changed files with 131 additions and 61 deletions

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-24 05:03+0100\n" "POT-Creation-Date: 2017-12-25 22:23+0100\n"
"PO-Revision-Date: 2017-12-24 05:02+0100\n" "PO-Revision-Date: 2017-12-24 05:02+0100\n"
"Last-Translator: Laura Klünder <laura@codingcatgirl.de>\n" "Last-Translator: Laura Klünder <laura@codingcatgirl.de>\n"
"Language-Team: \n" "Language-Team: \n"
@ -306,7 +306,7 @@ msgid "back"
msgstr "zurück" msgstr "zurück"
#: c3nav/control/templates/control/user.html:73 #: c3nav/control/templates/control/user.html:73
#: c3nav/mapdata/models/access.py:77 c3nav/mapdata/models/geometry/space.py:345 #: c3nav/mapdata/models/access.py:77 c3nav/mapdata/models/geometry/space.py:353
#: c3nav/site/models.py:17 #: c3nav/site/models.py:17
msgid "author" msgid "author"
msgstr "Autor" msgstr "Autor"
@ -428,45 +428,37 @@ msgstr "Token erfolgreich invalidiert."
msgid "You can only display your most recently created token." msgid "You can only display your most recently created token."
msgstr "Du kannst nur deinen zuletzt erstellten Code anzeigen." msgstr "Du kannst nur deinen zuletzt erstellten Code anzeigen."
#: c3nav/editor/forms.py:129 #: c3nav/editor/forms.py:128
msgid "Redirecting Slugs (comma seperated)" msgid "Redirecting Slugs (comma seperated)"
msgstr "Umleitungs-Slugs (mit Komma getrennt)" msgstr "Umleitungs-Slugs (mit Komma getrennt)"
#: c3nav/editor/forms.py:158 #: c3nav/editor/forms.py:157
#, python-format #, python-format
msgid "Can not add redirecting slug “%s”: it's the slug of this object." msgid "Can not add redirecting slug “%s”: it's the slug of this object."
msgstr "" msgstr ""
"Umleitungs-Slug „%s“ kann nicht hinzugefügt werden: Es ist der Slug dieses " "Umleitungs-Slug „%s“ kann nicht hinzugefügt werden: Es ist der Slug dieses "
"Objects." "Objects."
#: c3nav/editor/forms.py:165 #: c3nav/editor/forms.py:164
#, python-format #, python-format
msgid "Can not add redirecting slug “%s”: it is already used elsewhere." msgid "Can not add redirecting slug “%s”: it is already used elsewhere."
msgstr "" msgstr ""
"Umleitungs-Slug „%s“ kann nicht hinzugefügt werden: Er wird bereits an " "Umleitungs-Slug „%s“ kann nicht hinzugefügt werden: Er wird bereits an "
"anderer Stelle verwendet." "anderer Stelle verwendet."
#: c3nav/editor/forms.py:172 #: c3nav/editor/forms.py:171
msgid "Invalid JSON." msgid "Invalid JSON."
msgstr "Invalides JSON." msgstr "Invalides JSON."
#: c3nav/editor/forms.py:173 #: c3nav/editor/forms.py:230
msgid "Invalid Scan."
msgstr "Invalider Scan."
#: c3nav/editor/forms.py:178
msgid "Needs to be one scan at minimum."
msgstr "Muss mindestens 1 sein."
#: c3nav/editor/forms.py:252
msgid "Final rejection" msgid "Final rejection"
msgstr "Endgültige Ablehnung" msgstr "Endgültige Ablehnung"
#: c3nav/editor/forms.py:260 #: c3nav/editor/forms.py:238
msgid "create one way edges" msgid "create one way edges"
msgstr "Kante nur in eine Richtung erstellen" msgstr "Kante nur in eine Richtung erstellen"
#: c3nav/editor/forms.py:261 #: c3nav/editor/forms.py:239
msgid "activate next node after connecting" msgid "activate next node after connecting"
msgstr "nächsten Knoten nach dem Verbinden aktivieren" msgstr "nächsten Knoten nach dem Verbinden aktivieren"
@ -570,15 +562,15 @@ msgstr "Kartenupdate"
msgid "Change Sets" msgid "Change Sets"
msgstr "Änderungssets" msgstr "Änderungssets"
#: c3nav/editor/models/changeset.py:694 #: c3nav/editor/models/changeset.py:698
msgid "Direct editing active" msgid "Direct editing active"
msgstr "Direktes Bearbeiten aktiv" msgstr "Direktes Bearbeiten aktiv"
#: c3nav/editor/models/changeset.py:695 #: c3nav/editor/models/changeset.py:699
msgid "No objects changed" msgid "No objects changed"
msgstr "Keine Objekte geändert" msgstr "Keine Objekte geändert"
#: c3nav/editor/models/changeset.py:696 #: c3nav/editor/models/changeset.py:700
#, python-format #, python-format
msgid "%(num)d object changed" msgid "%(num)d object changed"
msgid_plural "%(num)d objects changed" msgid_plural "%(num)d objects changed"
@ -1471,7 +1463,7 @@ msgid "Doors"
msgstr "Türen" msgstr "Türen"
#: c3nav/mapdata/models/geometry/level.py:156 #: c3nav/mapdata/models/geometry/level.py:156
#: c3nav/mapdata/models/geometry/space.py:250 #: c3nav/mapdata/models/geometry/space.py:258
msgid "altitude" msgid "altitude"
msgstr "Bodenhöhe" msgstr "Bodenhöhe"
@ -1510,8 +1502,8 @@ msgstr ""
"erstellt." "erstellt."
#: c3nav/mapdata/models/geometry/space.py:23 #: c3nav/mapdata/models/geometry/space.py:23
#: c3nav/mapdata/models/geometry/space.py:266 #: c3nav/mapdata/models/geometry/space.py:274
#: c3nav/mapdata/models/geometry/space.py:303 #: c3nav/mapdata/models/geometry/space.py:311
msgid "space" msgid "space"
msgstr "Raum" msgstr "Raum"
@ -1590,65 +1582,65 @@ msgstr "Ort von Interesse"
msgid "Points of Interest" msgid "Points of Interest"
msgstr "Orte von Interesse" msgstr "Orte von Interesse"
#: c3nav/mapdata/models/geometry/space.py:240 #: c3nav/mapdata/models/geometry/space.py:248
msgid "Hole" msgid "Hole"
msgstr "Loch" msgstr "Loch"
#: c3nav/mapdata/models/geometry/space.py:241 #: c3nav/mapdata/models/geometry/space.py:249
msgid "Holes" msgid "Holes"
msgstr "Löcher" msgstr "Löcher"
#: c3nav/mapdata/models/geometry/space.py:253 #: c3nav/mapdata/models/geometry/space.py:261
msgid "Altitude Marker" msgid "Altitude Marker"
msgstr "Höhenmarker" msgstr "Höhenmarker"
#: c3nav/mapdata/models/geometry/space.py:254 #: c3nav/mapdata/models/geometry/space.py:262
msgid "Altitude Markers" msgid "Altitude Markers"
msgstr "Höhenmarker" msgstr "Höhenmarker"
#: c3nav/mapdata/models/geometry/space.py:267 #: c3nav/mapdata/models/geometry/space.py:275
#: c3nav/mapdata/models/geometry/space.py:306 #: c3nav/mapdata/models/geometry/space.py:314
msgid "target space" msgid "target space"
msgstr "Zielraum" msgstr "Zielraum"
#: c3nav/mapdata/models/geometry/space.py:269 #: c3nav/mapdata/models/geometry/space.py:277
#: c3nav/mapdata/models/geometry/space.py:308 c3nav/mapdata/models/graph.py:44 #: c3nav/mapdata/models/geometry/space.py:316 c3nav/mapdata/models/graph.py:44
msgid "description" msgid "description"
msgstr "Beschreibung" msgstr "Beschreibung"
#: c3nav/mapdata/models/geometry/space.py:272 #: c3nav/mapdata/models/geometry/space.py:280
msgid "Leave description" msgid "Leave description"
msgstr "Verlassensbeschreibung" msgstr "Verlassensbeschreibung"
#: c3nav/mapdata/models/geometry/space.py:273 #: c3nav/mapdata/models/geometry/space.py:281
msgid "Leave descriptions" msgid "Leave descriptions"
msgstr "Verlassensbeschreibungen" msgstr "Verlassensbeschreibungen"
#: c3nav/mapdata/models/geometry/space.py:304 #: c3nav/mapdata/models/geometry/space.py:312
msgid "origin space" msgid "origin space"
msgstr "Ursprungsraum" msgstr "Ursprungsraum"
#: c3nav/mapdata/models/geometry/space.py:311 #: c3nav/mapdata/models/geometry/space.py:319
msgid "Cross description" msgid "Cross description"
msgstr "Durchschreitungsbeschreibung" msgstr "Durchschreitungsbeschreibung"
#: c3nav/mapdata/models/geometry/space.py:312 #: c3nav/mapdata/models/geometry/space.py:320
msgid "Cross descriptions" msgid "Cross descriptions"
msgstr "Durchschreitungsbeschreibungen" msgstr "Durchschreitungsbeschreibungen"
#: c3nav/mapdata/models/geometry/space.py:346 #: c3nav/mapdata/models/geometry/space.py:354
msgid "comment" msgid "comment"
msgstr "Kommentar" msgstr "Kommentar"
#: c3nav/mapdata/models/geometry/space.py:347 #: c3nav/mapdata/models/geometry/space.py:355
msgid "Measurement list" msgid "Measurement list"
msgstr "Messungsliste" msgstr "Messungsliste"
#: c3nav/mapdata/models/geometry/space.py:350 #: c3nav/mapdata/models/geometry/space.py:358
msgid "Wi-Fi Measurement" msgid "Wi-Fi Measurement"
msgstr "WLAN Messung" msgstr "WLAN Messung"
#: c3nav/mapdata/models/geometry/space.py:351 #: c3nav/mapdata/models/geometry/space.py:359
msgid "Wi-Fi Measurements" msgid "Wi-Fi Measurements"
msgstr "WLAN Messungen" msgstr "WLAN Messungen"
@ -1982,6 +1974,10 @@ msgstr "Unbekannter Startort."
msgid "Unknown destination." msgid "Unknown destination."
msgstr "Unbekannter Zielort." msgstr "Unbekannter Zielort."
#: c3nav/routing/locator.py:129
msgid "Invalid Scan."
msgstr "Invalider Scan."
#: c3nav/routing/models.py:20 c3nav/routing/models.py:21 #: c3nav/routing/models.py:20 c3nav/routing/models.py:21
#: c3nav/site/templates/site/map.html:129 #: c3nav/site/templates/site/map.html:129
msgid "Route options" msgid "Route options"
@ -2035,12 +2031,12 @@ msgstr "komplett vermeiden"
msgid "avoid" msgid "avoid"
msgstr "vermeiden" msgstr "vermeiden"
#: c3nav/routing/models.py:135 #: c3nav/routing/models.py:142
#, python-format #, python-format
msgid "Unknown route option: %s" msgid "Unknown route option: %s"
msgstr "Unbekannte Routenoption: %s" msgstr "Unbekannte Routenoption: %s"
#: c3nav/routing/models.py:139 #: c3nav/routing/models.py:146
#, python-format #, python-format
msgid "Invalid value for route option %s." msgid "Invalid value for route option %s."
msgstr "Invalider Wert für Routenoption %s." msgstr "Invalider Wert für Routenoption %s."

View file

@ -1,3 +1,5 @@
import json
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.decorators import list_route from rest_framework.decorators import list_route
@ -5,9 +7,10 @@ from rest_framework.response import Response
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from c3nav.mapdata.models.access import AccessPermission from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.utils.locations import visible_locations_for_request from c3nav.mapdata.utils.locations import locations_for_request, visible_locations_for_request
from c3nav.routing.exceptions import LocationUnreachable, NoRouteFound, NotYetRoutable from c3nav.routing.exceptions import LocationUnreachable, NoRouteFound, NotYetRoutable
from c3nav.routing.forms import RouteForm from c3nav.routing.forms import RouteForm
from c3nav.routing.locator import Locator
from c3nav.routing.models import RouteOptions from c3nav.routing.models import RouteOptions
from c3nav.routing.router import Router from c3nav.routing.router import Router
@ -80,5 +83,18 @@ class RoutingViewSet(ViewSet):
@list_route(methods=('POST', )) @list_route(methods=('POST', ))
def locate(self, request, *args, **kwargs): def locate(self, request, *args, **kwargs):
# todo: implement wifi location try:
return Response({'location': None}) scan_data = json.loads(request.body)
except json.JSONDecodeError:
return Response({
'errors': (_('Invalid JSON'), ),
}, status=400)
try:
location = Locator.load().locate(scan_data, locations_for_request(request))
except ValidationError:
return Response({
'errors': (_('Invalid scan data.'),),
}, status=400)
return Response({'location': None if location is None else location.serialize()})

View file

@ -6,11 +6,14 @@ import threading
from collections import deque, namedtuple from collections import deque, namedtuple
from functools import reduce from functools import reduce
import numpy as np
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from c3nav.mapdata.models import MapUpdate, Space from c3nav.mapdata.models import MapUpdate, Space
from c3nav.mapdata.utils.locations import CustomLocation
from c3nav.routing.router import Router
class Locator: class Locator:
@ -25,10 +28,12 @@ class Locator:
stations = LocatorStations() stations = LocatorStations()
spaces = {} spaces = {}
for space in Space.objects.prefetch_related('wifi_measurements'): for space in Space.objects.prefetch_related('wifi_measurements'):
spaces[space.pk] = LocatorSpace( new_space = LocatorSpace(
LocatorPoint.from_measurement(measurement, stations) LocatorPoint.from_measurement(measurement, stations)
for measurement in space.wifi_measurements.all() for measurement in space.wifi_measurements.all()
) )
if new_space.points:
spaces[space.pk] = new_space
locator = cls(stations, spaces) locator = cls(stations, spaces)
pickle.dump(locator, open(cls.build_filename(update), 'wb')) pickle.dump(locator, open(cls.build_filename(update), 'wb'))
@ -56,18 +61,52 @@ class Locator:
cls.cached = cls.load_nocache(update) cls.cached = cls.load_nocache(update)
return cls.cached return cls.cached
def locate(self, scan, permissions=None):
router = Router.load()
restrictions = router.get_restrictions(permissions)
scan = LocatorPoint.validate_scan(scan, ignore_invalid_stations=True)
scan_values = LocatorPoint.convert_scan(scan, self.stations, create=False)
station_ids = frozenset(scan_values.keys())
spaces = tuple((pk, space, station_ids & space.stations_set)
for pk, space in self.spaces.items()
if pk not in restrictions.spaces)
spaces = tuple((pk, space, station_ids) for pk, space, station_ids in spaces if station_ids)
if not spaces:
return None
good_spaces = tuple((pk, space, station_ids) for pk, space, station_ids in spaces if len(station_ids) >= 3)
if not good_spaces:
for station_id in station_ids:
scan_values[station_id] = 0
good_spaces = spaces
good_spaces = sorted(good_spaces, key=lambda item: len(item[2]), reverse=True)[:10]
best_location = None
best_score = float('inf')
for space, station_ids in good_spaces:
point, score = space.get_best_point(scan_values, station_ids)
if score < best_score:
location = CustomLocation(router.spaces[space.pk].level, point.x, point.y)
best_location = location
best_score = score
return best_location
class LocatorStations: class LocatorStations:
def __init__(self): def __init__(self):
self.stations = [] self.stations = []
self.stations_lookup = {} self.stations_lookup = {}
def get_or_create(self, bssid, ssid, frequency): def get(self, bssid, ssid, frequency, create=False):
station_id = self.stations_lookup.get((bssid, ssid), None) station_id = self.stations_lookup.get((bssid, ssid), None)
if station_id is not None: if station_id is not None:
station = self.stations[station_id] station = self.stations[station_id]
station.frequencies.add(frequency) station.frequencies.add(frequency)
else: elif create:
station = LocatorStation(bssid, ssid, set((frequency, ))) station = LocatorStation(bssid, ssid, set((frequency, )))
station_id = len(self.stations) station_id = len(self.stations)
self.stations_lookup[(bssid, ssid)] = station_id self.stations_lookup[(bssid, ssid)] = station_id
@ -78,31 +117,45 @@ class LocatorStations:
class LocatorSpace: class LocatorSpace:
def __init__(self, points): def __init__(self, points):
self.points = tuple(points) self.points = tuple(points)
self.stations_set = reduce(operator.or_, (frozenset(point.values.keys()) for point in points), frozenset()) self.stations_set = reduce(operator.or_, (frozenset(point.values.keys()) for point in self.points), frozenset())
self.stations = tuple(self.stations_set) self.stations = tuple(self.stations_set)
self.stations_lookup = {station_id: i for i, station_id in enumerate(self.stations)} self.stations_lookup = {station_id: i for i, station_id in enumerate(self.stations)}
self.levels = np.full((len(self.points), len(self.stations)), fill_value=-100, dtype=np.int8)
for i, point in enumerate(self.points):
for station_id, value in point.values.items():
self.levels[i][self.stations_lookup[station_id]] = value
def get_best_point(self, scan_values, station_ids):
stations = tuple(self.stations_lookup[station_id] for station_id in station_ids)
values = np.array(tuple(scan_values[station_id] for station_id in station_ids), dtype=np.int8)
scores = np.sum((self.levels[:, stations]-values)**2, axis=1)
best_point = np.argmin(scores).ravel()[0]
return self.points[best_point], scores[best_point]
class LocatorPoint(namedtuple('LocatorPoint', ('x', 'y', 'values'))): class LocatorPoint(namedtuple('LocatorPoint', ('x', 'y', 'values'))):
@classmethod @classmethod
def from_measurement(cls, measurement, stations: LocatorStations): def from_measurement(cls, measurement, stations: LocatorStations):
return cls(x=measurement.geometry.x, y=measurement.geometry.y, return cls(x=measurement.geometry.x, y=measurement.geometry.y,
values=cls.convert_scans(measurement.data, stations)) values=cls.convert_scans(measurement.data, stations, create=True))
@classmethod @classmethod
def convert_scan(cls, scan, stations: LocatorStations): def convert_scan(cls, scan, stations: LocatorStations, create=False):
values = {} values = {}
for scan_value in scan: for scan_value in scan:
station_id = stations.get_or_create(scan_value['bssid'], scan_value['ssid'], scan_value['frequency']) station_id = stations.get(bssid=scan_value['bssid'], ssid=scan_value['ssid'],
frequency=scan_value['frequency'], create=create)
if station_id is not None:
# todo: convert to something more or less linear # todo: convert to something more or less linear
values[station_id] = scan_value['level'] values[station_id] = scan_value['level']
return values return values
@classmethod @classmethod
def convert_scans(cls, scans, stations: LocatorStations): def convert_scans(cls, scans, stations: LocatorStations, create=False):
values_list = deque() values_list = deque()
for scan in scans: for scan in scans:
values_list.append(cls.convert_scan(scan, stations)) values_list.append(cls.convert_scan(scan, stations, create))
station_ids = reduce(operator.or_, (frozenset(values.keys()) for values in values_list), frozenset()) station_ids = reduce(operator.or_, (frozenset(values.keys()) for values in values_list), frozenset())
return { return {
@ -125,18 +178,23 @@ class LocatorPoint(namedtuple('LocatorPoint', ('x', 'y', 'values'))):
allowed_keys = needed_keys | frozenset(('last', )) allowed_keys = needed_keys | frozenset(('last', ))
@classmethod @classmethod
def validate_scans(cls, data): def validate_scans(cls, data, ignore_invalid_stations=False):
if not isinstance(data, list): if not isinstance(data, list):
raise cls.invalid_scan raise cls.invalid_scan
for scan in data: return tuple(cls.validate_scan(scan) for scan in data)
cls.validate_scan(scan)
@classmethod @classmethod
def validate_scan(cls, data): def validate_scan(cls, data, ignore_invalid_stations=False):
if not isinstance(data, list): if not isinstance(data, list):
raise cls.invalid_scan raise cls.invalid_scan
cleaned_scan = deque()
for scan_value in data: for scan_value in data:
cls.validate_scan_value(scan_value) try:
cleaned_scan.append(cls.validate_scan_value(scan_value))
except ValidationError:
if not ignore_invalid_stations:
raise
return tuple(cleaned_scan)
@classmethod @classmethod
def validate_scan_value(cls, data): def validate_scan_value(cls, data):