2017-10-23 22:50:15 +02:00
|
|
|
import math
|
2017-10-24 00:11:02 +02:00
|
|
|
import os
|
2017-10-23 22:50:15 +02:00
|
|
|
import struct
|
2017-10-24 01:59:50 +02:00
|
|
|
from itertools import chain
|
2017-10-23 22:50:15 +02:00
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
from django.conf import settings
|
2017-10-23 19:25:15 +02:00
|
|
|
from django.db.models.signals import m2m_changed, post_delete
|
2017-10-24 13:59:42 +02:00
|
|
|
from PIL import Image
|
2017-10-23 22:50:15 +02:00
|
|
|
from shapely import prepared
|
|
|
|
from shapely.geometry import box
|
2017-10-24 00:11:02 +02:00
|
|
|
from shapely.ops import unary_union
|
2017-10-23 19:25:15 +02:00
|
|
|
|
2017-10-24 13:19:23 +02:00
|
|
|
from c3nav.mapdata.models import MapUpdate
|
2017-10-23 19:25:15 +02:00
|
|
|
from c3nav.mapdata.utils.models import get_submodels
|
|
|
|
|
|
|
|
|
2017-10-23 22:50:15 +02:00
|
|
|
class MapHistory:
|
|
|
|
# binary format (everything little-endian):
|
|
|
|
# 1 byte (uint8): resolution
|
|
|
|
# 2 bytes (uint16): origin x
|
|
|
|
# 2 bytes (uint16): origin y
|
|
|
|
# 2 bytes (uint16): origin width
|
|
|
|
# 2 bytes (uint16): origin height
|
|
|
|
# 2 bytes (uint16): number of updates
|
2017-10-24 01:59:50 +02:00
|
|
|
# n uptates times:
|
|
|
|
# 4 bytes (uint32): update id
|
|
|
|
# 4 bytes (uint32): timestamp
|
|
|
|
# width*height*2 bytes:
|
|
|
|
# data array (line after line) with uint16 cells
|
2017-10-24 13:12:17 +02:00
|
|
|
empty_array = np.empty((0, 0), dtype=np.uint16)
|
2017-10-23 22:50:15 +02:00
|
|
|
|
2017-10-24 14:46:35 +02:00
|
|
|
def __init__(self, resolution=settings.CACHE_RESOLUTION, x=0, y=0, updates=None, data=empty_array, filename=None):
|
2017-10-23 22:50:15 +02:00
|
|
|
self.resolution = resolution
|
2017-10-24 00:11:02 +02:00
|
|
|
self.x = x
|
|
|
|
self.y = y
|
2017-10-23 22:50:15 +02:00
|
|
|
self.updates = updates
|
|
|
|
self.data = data
|
2017-10-24 14:46:35 +02:00
|
|
|
self.filename = filename
|
2017-10-23 22:50:15 +02:00
|
|
|
self.unfinished = False
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def open(cls, filename, default_update=None):
|
|
|
|
try:
|
|
|
|
with open(filename, 'rb') as f:
|
|
|
|
resolution, x, y, width, height, num_updates = struct.unpack('<BHHHHH', f.read(11))
|
2017-10-24 13:26:25 +02:00
|
|
|
updates = struct.unpack('<'+'II'*num_updates, f.read(num_updates*8))
|
2017-10-24 01:59:50 +02:00
|
|
|
updates = list(zip(updates[0::2], updates[1::2]))
|
2017-10-24 00:11:02 +02:00
|
|
|
# noinspection PyTypeChecker
|
|
|
|
data = np.fromstring(f.read(width*height*2), np.uint16).reshape((height, width))
|
2017-10-24 14:46:35 +02:00
|
|
|
return cls(resolution, x, y, list(updates), data, filename)
|
2017-10-24 00:11:02 +02:00
|
|
|
except (FileNotFoundError, struct.error):
|
2017-10-23 22:50:15 +02:00
|
|
|
if default_update is None:
|
2017-10-24 13:19:23 +02:00
|
|
|
default_update = MapUpdate.last_update()
|
2017-10-24 00:21:57 +02:00
|
|
|
new_empty = cls(updates=[default_update])
|
|
|
|
new_empty.save(filename)
|
|
|
|
return new_empty
|
2017-10-23 22:50:15 +02:00
|
|
|
|
2017-10-24 14:46:35 +02:00
|
|
|
@staticmethod
|
|
|
|
def level_filename(level_id, mode):
|
|
|
|
return os.path.join(settings.CACHE_ROOT, 'level_%s_%d' % (mode, level_id))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def open_level(cls, level_id, mode, default_update=None):
|
|
|
|
return cls.open(cls.level_filename(level_id, mode), default_update)
|
|
|
|
|
|
|
|
def save(self, filename=None):
|
|
|
|
if filename is None:
|
|
|
|
filename = self.filename
|
2017-10-23 22:50:15 +02:00
|
|
|
with open(filename, 'wb') as f:
|
2017-10-24 13:59:42 +02:00
|
|
|
self.write(f)
|
|
|
|
|
|
|
|
def write(self, f):
|
|
|
|
f.write(struct.pack('<BHHHHH', self.resolution, self.x, self.y, *reversed(self.data.shape),
|
|
|
|
len(self.updates)))
|
|
|
|
f.write(struct.pack('<'+'II'*len(self.updates), *chain(*self.updates)))
|
|
|
|
f.write(self.data.tobytes('C'))
|
2017-10-23 22:50:15 +02:00
|
|
|
|
2017-10-24 15:29:43 +02:00
|
|
|
def add_new(self, geometry, data=None):
|
2017-10-23 22:50:15 +02:00
|
|
|
prep = prepared.prep(geometry)
|
|
|
|
minx, miny, maxx, maxy = geometry.bounds
|
|
|
|
res = self.resolution
|
|
|
|
minx = int(math.floor(minx/res))
|
|
|
|
miny = int(math.floor(miny/res))
|
|
|
|
maxx = int(math.ceil(maxx/res))
|
|
|
|
maxy = int(math.ceil(maxy/res))
|
|
|
|
|
2017-10-24 15:29:43 +02:00
|
|
|
direct = data is None
|
|
|
|
|
|
|
|
if direct:
|
|
|
|
data = self.data
|
|
|
|
if self.resolution != settings.CACHE_RESOLUTION:
|
|
|
|
data = None
|
|
|
|
self.updates = self.updates[-1:]
|
|
|
|
|
|
|
|
if not data.size:
|
|
|
|
data = np.zeros(((maxy-miny), (maxx-minx)), dtype=np.uint16)
|
|
|
|
self.x, self.y = minx, miny
|
|
|
|
else:
|
|
|
|
orig_height, orig_width = data.shape
|
|
|
|
if minx < self.x or miny < self.y or maxx > self.x+orig_width or maxy > self.y+orig_height:
|
|
|
|
new_x, new_y = min(minx, self.x), min(miny, self.y)
|
|
|
|
new_width = max(maxx, self.x+orig_width)-new_x
|
|
|
|
new_height = max(maxy, self.y+orig_height)-new_y
|
|
|
|
new_data = np.zeros((new_height, new_width), dtype=np.uint16)
|
|
|
|
dx, dy = self.x-new_x, self.y-new_y
|
|
|
|
new_data[dy:(dy+orig_height), dx:(dx+orig_width)] = data
|
|
|
|
data = new_data
|
|
|
|
self.x, self.y = new_x, new_y
|
2017-10-23 22:50:15 +02:00
|
|
|
else:
|
2017-10-24 15:29:43 +02:00
|
|
|
height, width = data.shape
|
|
|
|
minx, miny = max(minx, self.x), max(miny, self.y)
|
|
|
|
maxx, maxy = min(maxx, self.x+width), max(maxy, self.y+height)
|
|
|
|
|
|
|
|
new_val = len(self.updates) if direct else 1
|
2017-10-23 22:50:15 +02:00
|
|
|
for iy, y in enumerate(range(miny*res, maxy*res, res), start=miny-self.y):
|
2017-10-24 00:11:02 +02:00
|
|
|
for ix, x in enumerate(range(minx*res, maxx*res, res), start=minx-self.x):
|
2017-10-23 22:50:15 +02:00
|
|
|
if prep.intersects(box(x, y, x+res, y+res)):
|
|
|
|
data[iy, ix] = new_val
|
|
|
|
|
2017-10-24 15:29:43 +02:00
|
|
|
if direct:
|
|
|
|
self.data = data
|
|
|
|
self.unfinished = True
|
|
|
|
else:
|
|
|
|
return data
|
2017-10-23 22:50:15 +02:00
|
|
|
|
2017-10-24 01:59:50 +02:00
|
|
|
def finish(self, update):
|
2017-10-23 22:50:15 +02:00
|
|
|
self.unfinished = False
|
2017-10-24 01:59:50 +02:00
|
|
|
self.updates.append(update)
|
2017-10-24 13:05:08 +02:00
|
|
|
self.simplify()
|
|
|
|
|
|
|
|
def simplify(self):
|
|
|
|
# remove updates that have no longer any array cells
|
|
|
|
new_updates = ((update, (self.data == i)) for i, update in enumerate(self.updates))
|
|
|
|
self.updates, new_affected = zip(*((update, affected) for update, affected in new_updates if affected.any()))
|
|
|
|
for i, affected in enumerate(new_affected):
|
|
|
|
self.data[affected] = i
|
|
|
|
|
|
|
|
# remove borders
|
|
|
|
rows = self.data.any(axis=1).nonzero()[0]
|
|
|
|
if not rows.size:
|
2017-10-24 13:12:17 +02:00
|
|
|
self.data = self.empty_array
|
2017-10-24 13:05:08 +02:00
|
|
|
self.x = 0
|
|
|
|
self.y = 0
|
|
|
|
return
|
|
|
|
cols = self.data.any(axis=0).nonzero()[0]
|
|
|
|
miny, maxy = rows.min(), rows.max()
|
|
|
|
minx, maxx = cols.min(), cols.max()
|
|
|
|
self.x += minx
|
|
|
|
self.y += miny
|
|
|
|
self.data = self.data[miny:maxy+1, minx:maxx+1]
|
2017-10-23 22:50:15 +02:00
|
|
|
|
2017-10-24 15:29:43 +02:00
|
|
|
def composite(self, other, mask_geometry):
|
|
|
|
if other.resolution != other.resolution:
|
|
|
|
return
|
|
|
|
|
|
|
|
# check overlapping area
|
|
|
|
self_height, self_width = self.data.shape
|
|
|
|
other_height, other_width = other.data.shape
|
|
|
|
minx, miny = max(self.x, other.x), max(self.y, other.y)
|
|
|
|
maxx = min(self.x+self_width-1, other.x+other_width-1)
|
|
|
|
maxy = min(self.y+self_height-1, other.y+other_height-1)
|
|
|
|
if maxx < minx or maxy < miny:
|
|
|
|
return
|
|
|
|
|
|
|
|
# merge update lists
|
|
|
|
self_update_i = {update: i for i, update in enumerate(self.updates)}
|
|
|
|
other_update_i = {update: i for i, update in enumerate(other.updates)}
|
|
|
|
new_updates = sorted(set(self_update_i.keys()) | set(other_update_i.keys()))
|
|
|
|
|
|
|
|
# create slices
|
|
|
|
self_slice = slice(miny-self.y, maxy-self.y+1), slice(minx-self.x, maxx-self.x+1)
|
|
|
|
other_slice = slice(miny-other.y, maxy-other.y+1), slice(minx-other.x, maxx-other.x+1)
|
|
|
|
|
|
|
|
# reindex according to new update list
|
|
|
|
other_data = np.zeros_like(self.data)
|
|
|
|
other_data[self_slice] = other.data[other_slice]
|
|
|
|
for i, update in enumerate(new_updates):
|
|
|
|
if update in self_update_i:
|
|
|
|
self.data[self.data == self_update_i[update]] = i
|
|
|
|
if update in other_update_i:
|
|
|
|
other_data[other_data == other_update_i[update]] = i
|
|
|
|
|
|
|
|
# calculate maximum
|
|
|
|
maximum = np.maximum(self.data, other_data)
|
|
|
|
|
|
|
|
# add with mask
|
|
|
|
mask = self.add_new(mask_geometry.buffer(1), data=np.zeros_like(self.data, dtype=np.bool))
|
|
|
|
self.data[mask] = maximum[mask]
|
|
|
|
|
|
|
|
# write new updates
|
|
|
|
self.updates = new_updates
|
|
|
|
|
|
|
|
self.simplify()
|
|
|
|
|
2017-10-24 13:59:42 +02:00
|
|
|
def to_image(self):
|
|
|
|
from c3nav.mapdata.models import Source
|
|
|
|
(miny, minx), (maxy, maxx) = Source.max_bounds()
|
|
|
|
|
|
|
|
height, width = self.data.shape
|
|
|
|
image_data = np.zeros((int(math.ceil((maxy-miny)/self.resolution)),
|
|
|
|
int(math.ceil((maxx-minx)/self.resolution))), dtype=np.uint8)
|
|
|
|
visible_data = (self.data.astype(float)*255/(len(self.updates)-1)).clip(0, 255).astype(np.uint8)
|
|
|
|
image_data[self.y:self.y+height, self.x:self.x+width] = visible_data
|
|
|
|
|
|
|
|
return Image.fromarray(np.flip(image_data, axis=0), 'L')
|
|
|
|
|
2017-10-23 22:50:15 +02:00
|
|
|
|
2017-10-23 19:25:15 +02:00
|
|
|
class GeometryChangeTracker:
|
|
|
|
def __init__(self):
|
|
|
|
self._geometries_by_level = {}
|
|
|
|
self._deleted_levels = set()
|
|
|
|
|
|
|
|
def register(self, level_id, geometry):
|
|
|
|
self._geometries_by_level.setdefault(level_id, []).append(geometry)
|
|
|
|
|
|
|
|
def level_deleted(self, level_id):
|
|
|
|
self._deleted_levels.add(level_id)
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
self._geometries_by_level = {}
|
|
|
|
self._deleted_levels = set()
|
|
|
|
|
2017-10-24 00:11:02 +02:00
|
|
|
def save(self, last_update, new_update):
|
|
|
|
for level_id in self._deleted_levels:
|
|
|
|
try:
|
2017-10-24 14:46:35 +02:00
|
|
|
os.remove(MapHistory.level_filename(level_id, mode='base'))
|
2017-10-24 00:11:02 +02:00
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
|
|
|
self._geometries_by_level.pop(level_id, None)
|
|
|
|
|
|
|
|
for level_id, geometries in self._geometries_by_level.items():
|
|
|
|
geometries = unary_union(geometries)
|
|
|
|
if geometries.is_empty:
|
|
|
|
continue
|
2017-10-24 14:46:35 +02:00
|
|
|
history = MapHistory.open_level(level_id, mode='base', default_update=last_update)
|
2017-10-24 00:11:02 +02:00
|
|
|
history.add_new(geometries.buffer(1))
|
|
|
|
history.finish(new_update)
|
2017-10-24 14:46:35 +02:00
|
|
|
history.save()
|
2017-10-24 00:11:02 +02:00
|
|
|
self.reset()
|
|
|
|
|
2017-10-23 19:25:15 +02:00
|
|
|
|
|
|
|
changed_geometries = GeometryChangeTracker()
|
|
|
|
|
|
|
|
|
|
|
|
def geometry_deleted(sender, instance, **kwargs):
|
|
|
|
instance.register_delete()
|
|
|
|
|
|
|
|
|
|
|
|
def locationgroup_changed(sender, instance, action, reverse, model, pk_set, using, **kwargs):
|
|
|
|
if action not in ('post_add', 'post_remove', 'post_clear'):
|
|
|
|
return
|
|
|
|
|
|
|
|
if not reverse:
|
|
|
|
instance.register_change(force=True)
|
|
|
|
else:
|
|
|
|
if action not in 'post_clear':
|
|
|
|
raise NotImplementedError
|
|
|
|
query = model.objects.filter(pk__in=pk_set)
|
|
|
|
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
|
|
|
|
if issubclass(model, SpaceGeometryMixin):
|
|
|
|
query = query.select_related('space')
|
|
|
|
for obj in query:
|
|
|
|
obj.register_change(force=True)
|
|
|
|
|
|
|
|
|
|
|
|
def register_signals():
|
|
|
|
from c3nav.mapdata.models.geometry.base import GeometryMixin
|
|
|
|
for model in get_submodels(GeometryMixin):
|
|
|
|
post_delete.connect(geometry_deleted, sender=model)
|
|
|
|
|
|
|
|
from c3nav.mapdata.models.locations import SpecificLocation
|
|
|
|
for model in get_submodels(SpecificLocation):
|
|
|
|
m2m_changed.connect(locationgroup_changed, sender=model.groups.through)
|