2023-09-09 18:54:56 +02:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2020-03-21 22:40:39 +01:00
|
|
|
from owrx.config import Config
|
2019-07-14 19:32:48 +02:00
|
|
|
from owrx.bands import Band
|
2023-09-06 16:36:38 +02:00
|
|
|
from abc import abstractmethod, ABC, ABCMeta
|
2020-03-21 22:40:39 +01:00
|
|
|
import threading
|
|
|
|
|
import time
|
2020-01-05 18:41:46 +01:00
|
|
|
import sys
|
2019-07-07 15:52:24 +02:00
|
|
|
|
|
|
|
|
import logging
|
2019-07-21 19:40:28 +02:00
|
|
|
|
2019-07-07 15:52:24 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
2019-07-01 21:20:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Location(object):
|
2023-09-06 15:18:04 +02:00
|
|
|
def getTTL(self) -> timedelta:
|
|
|
|
|
pm = Config.get()
|
|
|
|
|
return timedelta(seconds=pm["map_position_retention_time"])
|
|
|
|
|
|
2019-07-01 21:20:53 +02:00
|
|
|
def __dict__(self):
|
2023-09-06 15:18:04 +02:00
|
|
|
return {
|
|
|
|
|
"ttl": self.getTTL().total_seconds() * 1000
|
|
|
|
|
}
|
2019-07-01 21:20:53 +02:00
|
|
|
|
|
|
|
|
|
2023-09-06 16:36:38 +02:00
|
|
|
class Source(ABC):
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def getKey(self) -> str:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def __dict__(self):
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
2019-07-01 21:20:53 +02:00
|
|
|
class Map(object):
|
|
|
|
|
sharedInstance = None
|
2020-01-05 18:41:46 +01:00
|
|
|
creationLock = threading.Lock()
|
2019-07-21 19:40:28 +02:00
|
|
|
|
2019-07-01 21:20:53 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def getSharedInstance():
|
2020-01-05 18:41:46 +01:00
|
|
|
with Map.creationLock:
|
|
|
|
|
if Map.sharedInstance is None:
|
|
|
|
|
Map.sharedInstance = Map()
|
2019-07-01 21:20:53 +02:00
|
|
|
return Map.sharedInstance
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.clients = []
|
|
|
|
|
self.positions = {}
|
2019-11-13 18:01:01 +01:00
|
|
|
self.positionsLock = threading.Lock()
|
2019-07-07 15:52:24 +02:00
|
|
|
|
|
|
|
|
def removeLoop():
|
2019-11-13 18:01:01 +01:00
|
|
|
loops = 0
|
2019-07-07 15:52:24 +02:00
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
self.removeOldPositions()
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.exception("error while removing old map positions")
|
2019-11-13 18:01:01 +01:00
|
|
|
loops += 1
|
|
|
|
|
# rebuild the positions dictionary every once in a while, it consumes lots of memory otherwise
|
|
|
|
|
if loops == 60:
|
|
|
|
|
try:
|
|
|
|
|
self.rebuildPositions()
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.exception("error while rebuilding positions")
|
|
|
|
|
loops = 0
|
2019-07-07 15:52:24 +02:00
|
|
|
time.sleep(60)
|
|
|
|
|
|
2020-08-14 20:22:25 +02:00
|
|
|
threading.Thread(target=removeLoop, daemon=True, name="map_removeloop").start()
|
2019-07-01 21:20:53 +02:00
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
|
|
def broadcast(self, update):
|
|
|
|
|
for c in self.clients:
|
|
|
|
|
c.write_update(update)
|
|
|
|
|
|
|
|
|
|
def addClient(self, client):
|
|
|
|
|
self.clients.append(client)
|
2019-07-21 19:40:28 +02:00
|
|
|
client.write_update(
|
|
|
|
|
[
|
|
|
|
|
{
|
2023-09-06 16:36:38 +02:00
|
|
|
"source": record["source"].__dict__(),
|
2019-07-21 19:40:28 +02:00
|
|
|
"location": record["location"].__dict__(),
|
|
|
|
|
"lastseen": record["updated"].timestamp() * 1000,
|
|
|
|
|
"mode": record["mode"],
|
|
|
|
|
"band": record["band"].getName() if record["band"] is not None else None,
|
|
|
|
|
}
|
2022-11-30 01:07:16 +01:00
|
|
|
for record in self.positions.values()
|
2019-07-21 19:40:28 +02:00
|
|
|
]
|
|
|
|
|
)
|
2019-07-01 21:20:53 +02:00
|
|
|
|
|
|
|
|
def removeClient(self, client):
|
|
|
|
|
try:
|
|
|
|
|
self.clients.remove(client)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
|
2023-09-09 18:54:56 +02:00
|
|
|
def updateLocation(self, source: Source, loc: Location, mode: str, band: Band = None, timestamp: datetime = None):
|
|
|
|
|
if timestamp is None:
|
|
|
|
|
timestamp = datetime.now(timezone.utc)
|
|
|
|
|
else:
|
|
|
|
|
# if we get an external timestamp, make sure it's not already expired
|
|
|
|
|
if datetime.now(timezone.utc) - loc.getTTL() > timestamp:
|
|
|
|
|
return
|
2023-09-06 16:36:38 +02:00
|
|
|
key = source.getKey()
|
2019-11-13 18:01:01 +01:00
|
|
|
with self.positionsLock:
|
2023-08-25 21:15:29 +02:00
|
|
|
if isinstance(loc, IncrementalUpdate) and key in self.positions:
|
|
|
|
|
loc.update(self.positions[key]["location"])
|
2023-09-09 18:54:56 +02:00
|
|
|
self.positions[key] = {"source": source, "location": loc, "updated": timestamp, "mode": mode, "band": band}
|
2019-07-21 19:40:28 +02:00
|
|
|
self.broadcast(
|
|
|
|
|
[
|
|
|
|
|
{
|
2023-09-06 16:36:38 +02:00
|
|
|
"source": source.__dict__(),
|
2019-07-21 19:40:28 +02:00
|
|
|
"location": loc.__dict__(),
|
2023-09-09 18:54:56 +02:00
|
|
|
"lastseen": timestamp.timestamp() * 1000,
|
2019-07-21 19:40:28 +02:00
|
|
|
"mode": mode,
|
|
|
|
|
"band": band.getName() if band is not None else None,
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
)
|
2019-07-07 15:52:24 +02:00
|
|
|
|
2023-09-06 16:36:38 +02:00
|
|
|
def touchLocation(self, source: Source):
|
2019-09-23 16:51:38 +02:00
|
|
|
# not implemented on the client side yet, so do not use!
|
2023-09-09 18:54:56 +02:00
|
|
|
ts = datetime.now(timezone.utc)
|
2023-09-06 16:36:38 +02:00
|
|
|
key = source.getKey()
|
2019-11-13 18:01:01 +01:00
|
|
|
with self.positionsLock:
|
2022-11-30 01:07:16 +01:00
|
|
|
if key in self.positions:
|
|
|
|
|
self.positions[key]["updated"] = ts
|
2023-09-06 16:36:38 +02:00
|
|
|
self.broadcast([{"source": source.__dict__(), "lastseen": ts.timestamp() * 1000}])
|
2019-09-23 16:51:38 +02:00
|
|
|
|
2022-11-30 01:07:16 +01:00
|
|
|
def removeLocation(self, key):
|
2019-11-13 18:01:01 +01:00
|
|
|
with self.positionsLock:
|
2022-11-30 01:07:16 +01:00
|
|
|
del self.positions[key]
|
2019-11-13 18:01:01 +01:00
|
|
|
# TODO broadcast removal to clients
|
2019-07-07 15:52:24 +02:00
|
|
|
|
|
|
|
|
def removeOldPositions(self):
|
2023-09-09 18:54:56 +02:00
|
|
|
now = datetime.now(timezone.utc)
|
2023-08-26 16:23:03 +02:00
|
|
|
|
|
|
|
|
with self.positionsLock:
|
2023-09-06 15:18:04 +02:00
|
|
|
to_be_removed = [
|
|
|
|
|
key for (key, pos) in self.positions.items() if now - pos["location"].getTTL() > pos["updated"]
|
|
|
|
|
]
|
2022-11-30 01:07:16 +01:00
|
|
|
for key in to_be_removed:
|
|
|
|
|
self.removeLocation(key)
|
2019-07-01 21:20:53 +02:00
|
|
|
|
2019-11-13 18:01:01 +01:00
|
|
|
def rebuildPositions(self):
|
2020-01-05 18:41:46 +01:00
|
|
|
logger.debug("rebuilding map storage; size before: %i", sys.getsizeof(self.positions))
|
2019-11-13 18:01:01 +01:00
|
|
|
with self.positionsLock:
|
|
|
|
|
p = {key: value for key, value in self.positions.items()}
|
|
|
|
|
self.positions = p
|
2020-01-05 18:41:46 +01:00
|
|
|
logger.debug("rebuild complete; size after: %i", sys.getsizeof(self.positions))
|
2019-11-13 18:01:01 +01:00
|
|
|
|
2019-07-21 19:40:28 +02:00
|
|
|
|
2019-07-01 21:20:53 +02:00
|
|
|
class LatLngLocation(Location):
|
2019-09-18 18:50:48 +02:00
|
|
|
def __init__(self, lat: float, lon: float):
|
2019-07-01 21:20:53 +02:00
|
|
|
self.lat = lat
|
|
|
|
|
self.lon = lon
|
|
|
|
|
|
|
|
|
|
def __dict__(self):
|
2023-09-06 15:18:04 +02:00
|
|
|
res = super().__dict__()
|
|
|
|
|
res.update(
|
|
|
|
|
{"type": "latlon", "lat": self.lat, "lon": self.lon}
|
|
|
|
|
)
|
2019-08-11 18:42:41 +02:00
|
|
|
return res
|
2019-07-01 21:20:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class LocatorLocation(Location):
|
|
|
|
|
def __init__(self, locator: str):
|
|
|
|
|
self.locator = locator
|
|
|
|
|
|
|
|
|
|
def __dict__(self):
|
2023-09-06 15:18:04 +02:00
|
|
|
res = super().__dict__()
|
|
|
|
|
res.update(
|
|
|
|
|
{"type": "locator", "locator": self.locator}
|
|
|
|
|
)
|
|
|
|
|
return res
|
2023-08-25 21:15:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class IncrementalUpdate(Location, metaclass=ABCMeta):
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def update(self, previousLocation: Location):
|
|
|
|
|
pass
|
2023-09-06 16:36:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallsignSource(Source):
|
|
|
|
|
def __init__(self, callsign: str):
|
|
|
|
|
self.callsign = callsign
|
|
|
|
|
|
|
|
|
|
def getKey(self) -> str:
|
|
|
|
|
return "callsign:{}".format(self.callsign)
|
|
|
|
|
|
|
|
|
|
def __dict__(self):
|
|
|
|
|
return {"callsign": self.callsign}
|