first implementation of wifi-based location
This commit is contained in:
parent
03018a2f46
commit
580c4f0b85
3 changed files with 131 additions and 61 deletions
|
@ -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."
|
||||
|
|
|
@ -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()})
|
||||
|
|
|
@ -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'])
|
||||
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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue