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

View file

@ -1,3 +1,5 @@
import json
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from rest_framework.decorators import list_route
@ -5,9 +7,10 @@ from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
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.forms import RouteForm
from c3nav.routing.locator import Locator
from c3nav.routing.models import RouteOptions
from c3nav.routing.router import Router
@ -80,5 +83,18 @@ class RoutingViewSet(ViewSet):
@list_route(methods=('POST', ))
def locate(self, request, *args, **kwargs):
# todo: implement wifi location
return Response({'location': None})
try:
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 functools import reduce
import numpy as np
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from c3nav.mapdata.models import MapUpdate, Space
from c3nav.mapdata.utils.locations import CustomLocation
from c3nav.routing.router import Router
class Locator:
@ -25,10 +28,12 @@ class Locator:
stations = LocatorStations()
spaces = {}
for space in Space.objects.prefetch_related('wifi_measurements'):
spaces[space.pk] = LocatorSpace(
new_space = LocatorSpace(
LocatorPoint.from_measurement(measurement, stations)
for measurement in space.wifi_measurements.all()
)
if new_space.points:
spaces[space.pk] = new_space
locator = cls(stations, spaces)
pickle.dump(locator, open(cls.build_filename(update), 'wb'))
@ -56,18 +61,52 @@ class Locator:
cls.cached = cls.load_nocache(update)
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:
def __init__(self):
self.stations = []
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)
if station_id is not None:
station = self.stations[station_id]
station.frequencies.add(frequency)
else:
elif create:
station = LocatorStation(bssid, ssid, set((frequency, )))
station_id = len(self.stations)
self.stations_lookup[(bssid, ssid)] = station_id
@ -78,31 +117,45 @@ class LocatorStations:
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_set = reduce(operator.or_, (frozenset(point.values.keys()) for point in self.points), frozenset())
self.stations = tuple(self.stations_set)
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'))):
@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))
values=cls.convert_scans(measurement.data, stations, create=True))
@classmethod
def convert_scan(cls, scan, stations: LocatorStations):
def convert_scan(cls, scan, stations: LocatorStations, create=False):
values = {}
for scan_value in scan:
station_id = stations.get_or_create(scan_value['bssid'], scan_value['ssid'], scan_value['frequency'])
# todo: convert to something more or less linear
values[station_id] = scan_value['level']
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
values[station_id] = scan_value['level']
return values
@classmethod
def convert_scans(cls, scans, stations: LocatorStations):
def convert_scans(cls, scans, stations: LocatorStations, create=False):
values_list = deque()
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())
return {
@ -125,18 +178,23 @@ class LocatorPoint(namedtuple('LocatorPoint', ('x', 'y', 'values'))):
allowed_keys = needed_keys | frozenset(('last', ))
@classmethod
def validate_scans(cls, data):
def validate_scans(cls, data, ignore_invalid_stations=False):
if not isinstance(data, list):
raise cls.invalid_scan
for scan in data:
cls.validate_scan(scan)
return tuple(cls.validate_scan(scan) for scan in data)
@classmethod
def validate_scan(cls, data):
def validate_scan(cls, data, ignore_invalid_stations=False):
if not isinstance(data, list):
raise cls.invalid_scan
cleaned_scan = deque()
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
def validate_scan_value(cls, data):