From 408d7e2bd77859ee03845855acc9010308d18ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Fri, 17 Nov 2017 13:56:15 +0100 Subject: [PATCH] Introducing GeometryIndexed as a base class for MapHistory --- src/c3nav/mapdata/cache.py | 349 +++++++++++++++++++++---------------- 1 file changed, 196 insertions(+), 153 deletions(-) diff --git a/src/c3nav/mapdata/cache.py b/src/c3nav/mapdata/cache.py index eea4a0ee..c57c8b77 100644 --- a/src/c3nav/mapdata/cache.py +++ b/src/c3nav/mapdata/cache.py @@ -3,7 +3,6 @@ import math import os import struct import threading -import traceback from itertools import chain import numpy as np @@ -12,6 +11,7 @@ from django.db.models.signals import m2m_changed, post_delete from PIL import Image from shapely import prepared from shapely.geometry import box +from shapely.geometry.base import BaseGeometry from shapely.ops import unary_union from c3nav.mapdata.models import MapUpdate @@ -20,47 +20,197 @@ from c3nav.mapdata.utils.models import get_submodels logger = logging.getLogger('c3nav') -class MapHistory: +class GeometryIndexed: # binary format (everything little-endian): + # 1 byte (uint8): variant id # 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 - # n uptates times: - # 4 bytes (uint32): update id - # 4 bytes (uint32): timestamp - # width*height*2 bytes: - # data array (line after line) with uint16 cells - empty_array = np.empty((0, 0), dtype=np.uint16) + # (optional meta data, depending on subclass) + # x bytes data, line after line. (cell size depends on subclass) + dtype = np.uint16 + variant_id = 0 - def __init__(self, resolution=settings.CACHE_RESOLUTION, x=0, y=0, updates=None, data=empty_array, filename=None): + def __init__(self, resolution=settings.CACHE_RESOLUTION, x=0, y=0, data=None, filename=None): self.resolution = resolution self.x = x self.y = y - self.updates = updates - self.data = data + self.data = data if data is not None else self._get_empty_array() self.filename = filename + + @classmethod + def _get_empty_array(cls): + return np.empty((0, 0), dtype=cls.dtype) + + @classmethod + def open(cls, filename): + with open(filename, 'rb') as f: + instance = cls.read(f) + instance.filename = filename + return instance + + @classmethod + def read(cls, f): + variant_id, resolution, x, y, width, height = struct.unpack(' self.x+orig_width or maxy > self.y+orig_height: - logging.info('resize!') - 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 - logging.info('resized dx=%d, dy=%d, x=%d, y=%d, shape=%s' % - (dx, dy, self.x, self.y, data.shape)) - - else: - logging.info('not direct!') - height, width = data.shape - minx, miny = max(minx, self.x), max(miny, self.y) - maxx, maxy = min(maxx, self.x+width), min(maxy, self.y+height) - - new_val = len(self.updates) if direct else 1 - i = 0 - for iy, y in enumerate(range(miny*res, maxy*res, res), start=miny-self.y): - for ix, x in enumerate(range(minx*res, maxx*res, res), start=minx-self.x): - if prep.intersects(box(x, y, x+res, y+res)): - data[iy, ix] = new_val - i += 1 - logging.info('%d points changed' % i) - - if direct: - logging.info('saved data') - self.data = data - self.unfinished = True - else: - return data - - def finish(self, update): - self.unfinished = False - self.updates.append(update) - self.simplify() + self[geometry] = len(self.updates) - 1 def simplify(self): - logging.info('simplify!') # remove updates that have no longer any array cells new_updates = ((i, update, (self.data == i)) for i, update in enumerate(self.updates)) - logging.info('before: %s' % (self.updates, )) self.updates, new_affected = zip(*((update, affected) for i, update, affected in new_updates if i == 0 or affected.any())) - logging.info('after: %s' % (self.updates, )) for i, affected in enumerate(new_affected): self.data[affected] = i - def composite(self, other, mask_geometry): - if other.resolution != other.resolution: - return + def write(self, *args, **kwargs): + self.simplify() + super().write(*args, **kwargs) - # 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 + def composite(self, other, mask_geometry): + if self.resolution != other.resolution: + raise ValueError('Cannot composite with different resolutions.') + + self.fit_bounds(*other.bounds) + other.fit_bounds(*self.bounds) # 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] + # reindex according to merged update list + other_data = other.data.copy() for i, update in enumerate(new_updates): if update in self_update_i: self.data[self.data == self_update_i[update]] = i @@ -221,38 +283,20 @@ class MapHistory: # add with mask if mask_geometry is not None: - mask = self.add_new(mask_geometry.buffer(1), data=np.zeros_like(self.data, dtype=np.bool)) + mask = self.get_geometry_cells(mask_geometry) self.data[mask] = maximum[mask] else: self.data = maximum # write new updates self.updates = new_updates - self.simplify() - def to_image(self): - from c3nav.mapdata.models import Source - (minx, miny), (maxx, maxy) = 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') - def last_update(self, minx, miny, maxx, maxy): - res = self.resolution - height, width = self.data.shape - minx = max(int(math.floor(minx/res)), self.x)-self.x - miny = max(int(math.floor(miny/res)), self.y)-self.y - maxx = min(int(math.ceil(maxx/res)), self.x+width)-self.x - maxy = min(int(math.ceil(maxy/res)), self.y+height)-self.y - if minx >= maxx or miny >= maxy: - return self.updates[0] - return self.updates[self.data[miny:maxy, minx:maxx].max()] + cells = self[box(minx, miny, maxx, maxy)] + if cells.size: + return self.updates[cells.max()] + return self.updates[0] class GeometryChangeTracker: @@ -299,8 +343,7 @@ class GeometryChangeTracker: if geometries.is_empty: continue history = MapHistory.open_level(level_id, mode='base', default_update=last_update) - history.add_new(geometries.buffer(1)) - history.finish(new_update) + history.add_geometry(geometries.buffer(1), new_update) history.save() self.reset()