Source code for tracklib.core.SpatialIndex


from __future__ import annotations   
from typing import Any

import math
import pickle
import progressbar

from tracklib.core.Bbox import Bbox
from tracklib.core.Track import Track
from tracklib.core.Network import Edge
from tracklib.core.TrackCollection import TrackCollection
from tracklib.core.ObsCoords import GeoCoords, ENUCoords

import tracklib.plot.IPlotVisitor as ivisitor
import tracklib.util.Geometry as Geometry


[docs]class SpatialIndex: """ This module contains the class to manipulate a spatial Index. """
[docs] def __init__(self, collection, resolution=None, margin=0.05, verbose=True): """Constructor of :class:`SaptialIndex` class TODO: update documentation Parameters ---------- features : bbox() + iterable TrackCollection : on construit une grille dont l’emprise est calculée sur la fonction getBBox de TrackCollection et dans un deuxième temps on appelle addSegment([c1,c2], [i,j]) pour chaque segment [c1,c2] (localisé entre les points GPS i et i+1) de chaque trace j, de la collection. Network : on construit une grille (de taille 100 x 100) par défaut, dont l’emprise est calculée sur celle du réseau, et on appelle addSegment ([c1,c2], [i,j]) pour chaque segment [c1,c2] (localisé entre les points GPS i et i+1) de chaque tronçon j, du réseau. resolution : tuple (xsize, ysize) DESCRIPTION. The default is (100, 100). Returns ------- None. """ # Bbox only or collection if isinstance(collection, Bbox): bb = collection else: bb = collection.bbox() bb = bb.copy() bb.addMargin(margin) (self.xmin, self.xmax, self.ymin, self.ymax) = bb.asTuple() ax, ay = bb.getDimensions() if resolution is None: am = max(ax, ay) r = am / 100 resolution = (int(ax / r), int(ay / r)) else: r = resolution resolution = (int(ax / r[0]), int(ay / r[1])) self.collection = collection # Keeps track of registered features self.inventaire = set() # Nombre de dalles par cote self.csize = resolution[0] self.lsize = resolution[1] # print ('nb cellule', self.xsize * self.ysize) # Tableau de collections de features appartenant a chaque dalle. # Un feature peut appartenir a plusieurs dalles. self.grid = [] for i in range(self.csize): self.grid.append([]) for j in range(self.lsize): self.grid[i].append([]) self.dX = ax / self.csize self.dY = ay / self.lsize # Calcul de la grille if isinstance(collection, tuple): self.collection = TrackCollection() return boucle = range(collection.size()) if verbose: print( "Building [" + str(self.csize) + " x " + str(self.lsize) + "] spatial index..." ) boucle = progressbar.progressbar(boucle) for num in boucle: feature = collection[num] # On récupere la trace if isinstance(feature, Track): self.addFeature(feature, num) # On récupère l'arc du reseau qui est une trace elif isinstance(feature, Edge): self.addFeature(feature.geom, num)
[docs] def __str__(self): """TODO""" c = [(self.xmin + self.xmax) / 2.0, (self.ymin + self.ymax) / 2.0] output = "[" + str(self.csize) + " x " + str(self.lsize) + "] " output += "spatial index centered on [" + str(c[0]) + "; " + str(c[1]) + "]" return output
[docs] def addFeature(self, track, num): """TODO""" coord1 = None for i in range(track.size()): obs = track.getObs(i) coord2 = obs.position if coord1 != None: p1 = self.__getCell(coord1) p2 = self.__getCell(coord2) if p1 is None or p2 is None: continue self.__addSegment(p1, p2, num) coord1 = coord2
def __addSegment(self, coord1, coord2, data): """TODO data de type: int, liste, tuple, dictionnaire ajoute les données data dans toutes les cellules de la grille traversée par le segment [coord1, coord2] avec coord1 : indices de la grille """ CELLS = self.__cellsCrossSegment(coord1, coord2) if CELLS is None: return # out of grid # print (CELLS, coord1, coord2) for cell in CELLS: i = cell[0] j = cell[1] if i > self.csize: print("error, depassement en x") exit() if j > self.lsize: print("error, depassement en y") exit() if data not in self.grid[i][j]: if (i, j, data) not in self.inventaire: self.grid[i][j].append(data) self.inventaire.add((i, j, data)) def __addPoint(self, coord, data): """TODO""" pass # ------------------------------------------------------------ # Normalized coordinates of coord: (x,) -> (i,j) with: # i = (x-xmin)/(xmax-xmin)*nb_cols # j = (y-ymin)/(ymax-ymin)*nb_rows # Returns None if out of grid # ------------------------------------------------------------ def __getCell(self, coord): """TODO""" if (coord.getX() < self.xmin) or (coord.getX() > self.xmax): overflow = "{:5.5f}".format( max(self.xmin - coord.getX(), coord.getX() - self.xmax) ) print("Warning: x overflow " + str(coord) + " OVERFLOW = " + str(overflow)) return None if (coord.getY() < self.ymin) or (coord.getY() > self.ymax): overflow = "{:5.5f}".format( max(self.ymin - coord.getY(), coord.getY() - self.ymax) ) print("Warning: y overflow " + str(coord) + " OVERFLOW = " + str(overflow)) return None idx = (float(coord.getX()) - self.xmin) / self.dX idy = (float(coord.getY()) - self.ymin) / self.dY # idy = self.nrow - idy ?? return (idx, idy)
[docs] def plot(self, base:bool=True, append=True, v:ivisitor.IPlotVisitor=None): if v == None: import tracklib.plot.MatplotlibVisitor as visitor v = visitor.MatplotlibVisitor() v.plotSpatialIndex(self, base, append)
[docs] def highlight(self, i, j, v:ivisitor.IPlotVisitor=None, sym="r-", size=0.5): if v == None: import tracklib.plot.MatplotlibVisitor as visitor v = visitor.MatplotlibVisitor() v.highlightCellInSpatialIndex(self, i, j, sym, size)
[docs] def request(self, obj, j=None) -> list[Any]: """ Request function to get data registered in spatial index Inputs: - request(i,j) returns data registered in cell (i,j) - i: row index i of spatial index grid - j: col index j of spatial index grid - request(coord) returns data registered in the cell containing GeoCoords or ENUCoors object coord - request(list) returns data registered in all cells crossed by a segment list=[coord1, coord2]. - request(track) returns data registered in all cells crossed by a track. """ if isinstance(obj, int): """dans la cellule (i,j)""" i = obj return self.grid[i][j] if isinstance(obj, GeoCoords) or isinstance(obj, ENUCoords): """dans la cellule contenant le point coord""" coord = obj c = self.__getCell(coord) return self.request(math.floor(c[0]), math.floor(c[1])) if isinstance(obj, list): """dans les cellules traversées par le segment défini par des coordonnées géographiques""" [coord1, coord2] = obj p1 = self.__getCell(coord1) p2 = self.__getCell(coord2) # Les cellules traversées par le segment CELLS = self.__cellsCrossSegment(p1, p2) TAB = [] for cell in CELLS: self.__addCellValuesInTAB(TAB, cell) return TAB if isinstance(obj, Track): """dans les cellules traversée par la track""" track = obj # récupération des cellules de la track TAB = [] pos1 = None for i in range(track.size()): obs = track.getObs(i) pos2 = obs.position if pos1 != None: coord1 = self.__getCell(pos1) coord2 = self.__getCell(pos2) CELLS = self.__cellsCrossSegment(coord1, coord2) for cell in CELLS: self.__addCellValuesInTAB(TAB, cell) pos1 = pos2 return TAB
# ------------------------------------------------------------ # Neighborhood function to get all data registered in spatial # index and located in the vicinity of a given location. # ------------------------------------------------------------ # - neighborhood(i,j,unit) returns all data (as a plain list) # registered in a cell located at less than 'unit' distance # from (i,j) cell. # - neighborhood(coord, unit) returns data (as a plain list) # registered in a cells located at less than 'unit' distance # from cell containing coord. # - neighborhood([c1, c2], unit) returns data (as a plain # list) registered in a cells located at less than 'unit' # distance from cells containing segment [c1, c2] # - neighborhood(track, unit) returns data (as a plain list) # registered in a cells located at less than 'unit' distance # from cells containing track # ------------------------------------------------------------ # As default value, unit=0, meaning that only data located in # (i,j) cell are selected. If unit=-1, the minimal value is # selected in order to get at least 1 data in function output. # ------------------------------------------------------------ # The number of cells inspected is given by: # - (1+2*unit)^2 if unit >= 0 # - (1+2*(unit'+1))^2 if unit < 0, with unit' is the min # value of unit such that output is not empty # ------------------------------------------------------------
[docs] def neighborhood(self, obj, j=None, unit=0): """TODO retourne toutes les données (sous forme de liste simple) référencées dans la cellule (i,j). Si unit=-1, calcule la valeur minimale à donner à unit, pour que la liste ne soit pas vide*. """ # -------------------------------------------------------- # neighborhood(i,j,unit) # -------------------------------------------------------- if isinstance(obj, int): i = obj if unit != -1: TAB = set() NC = self.__neighboringcells(i, j, unit, False) for cell in NC: TAB.update(self.request(cell[0], cell[1])) return list(TAB) # ----------------------------------------- # Case: unit < 0 -> search for unit value # ----------------------------------------- u = 0 TAB = set() found = False while u <= max(self.csize, self.lsize): NC = self.__neighboringcells(i, j, u, True) for cell in NC: TAB.update(self.request(cell[0], cell[1])) if found: break found = len(TAB) > 0 u += 1 return list(TAB) # -------------------------------------------------------- # neighborhood(coord, unit) # -------------------------------------------------------- if isinstance(obj, GeoCoords) or isinstance(obj, ENUCoords): coord = obj x = coord.getX() y = coord.getY() c = self.__getCell(ENUCoords(x, y)) if c != None: return self.neighborhood(math.floor(c[0]), math.floor(c[1]), unit) return None #return self.neighborhood(c, unit) # -------------------------------------------------------- # neighborhood([c1, c2], unit) # -------------------------------------------------------- if isinstance(obj, list): """cellules voisines traversées par le segment coord""" [coord1, coord2] = obj p1 = self.__getCell(coord1) p2 = self.__getCell(coord2) if unit > -1: # Tableau à retourner TAB = [] # Les cellules traversées par le segment CELLS = self.__cellsCrossSegment(p1, p2) for cell in CELLS: NC = self.__neighboringcells(cell[0], cell[1], unit) # print (' ', cell, NC) for cellu in NC: self.__addCellValuesInTAB(TAB, cellu) return TAB u = 0 while u <= max(self.csize, self.lsize): TAB = [] CELLS = self.__cellsCrossSegment(p1, p2) for cell in CELLS: NC = self.__neighboringcells(cell[0], cell[1], u) # print (cell, NC) for cellu in NC: self.__addCellValuesInTAB(TAB, cellu) # print (TAB) if len(TAB) <= 0: u += 1 continue # Plus une marge de sécurité CELLS = self.__cellsCrossSegment(p1, p2) for cell in CELLS: NC = self.__neighboringcells(cell[0], cell[1], u + 1) # print (cell, NC) for cellu in NC: self.__addCellValuesInTAB(TAB, cellu) # print (TAB) return TAB # -------------------------------------------------------- # neighborhood(track, unit) # -------------------------------------------------------- if isinstance(obj, Track): """cellules voisines traversées par Track""" track = obj TAB2 = [] pos1 = None for i in range(track.size()): obs = track.getObs(i) pos2 = obs.position if pos1 != None: CELLS = self.neighborhood([pos1, pos2], None, unit) # print (CELLS, unit) for cell in CELLS: if cell not in TAB2: TAB2.append(cell) pos1 = pos2 return TAB2
# ------------------------------------------------------------ # Function to convert ground distance (metric system is # assumed to be orthonormal) into unit number # ------------------------------------------------------------
[docs] def groundDistanceToUnits(self, distance): """TODO""" return math.floor(distance / max(self.dX, self.dY) + 1)
# ------------------------------------------------------------ # Returns all cells (i',j') in a vicinity unit of (i,j) # ------------------------------------------------------------ # - incremental = True: gets all cells where distance is to # central cell is exactly u units (Manhattan L1 discretized # distance). Used for incremental search of neighbors. # - incremental = False: gets all cells where distance is less # or equal than u units (Manhattan L1 discretized distance) # ------------------------------------------------------------ def __neighboringcells(self, i, j, u=0, incremental=False): """TODO""" NC = [] imin = max(i - u, 0) imax = min(i + u + 1, self.csize) jmin = max(j - u, 0) jmax = min(j + u + 1, self.lsize) for ii in range(imin, imax): for jj in range(jmin, jmax): if incremental: if (ii != imin) and (ii != imax - 1): if (jj != jmin) and (jj != jmax - 1): continue NC.append((ii, jj)) return NC # ------------------------------------------------------------ # Add data registered in cell within TAB structure # ------------------------------------------------------------ def __addCellValuesInTAB(self, TAB, cell): """TODO""" values = self.request(cell[0], cell[1]) for d in values: if d not in TAB: TAB.append(d) # ------------------------------------------------------------ # List of cells crossing segment [coord1, coord2] (in px) # ------------------------------------------------------------ def __cellsCrossSegment(self, coord1, coord2): """TODO""" CELLS = [] segment2 = [coord1[0], coord1[1], coord2[0], coord2[1]] xmin = min(math.floor(coord1[0]), math.floor(coord2[0])) xmax = max(math.floor(coord1[0]), math.floor(coord2[0])) ymin = min(math.floor(coord1[1]), math.floor(coord2[1])) ymax = max(math.floor(coord1[1]), math.floor(coord2[1])) for i in range(xmin, xmax + 1): for j in range(ymin, ymax + 1): # complètement inclus if ( i < coord1[0] and coord1[0] < i + 1 and i < coord2[0] and coord2[0] < i + 1 and j < coord1[1] and coord1[1] < j + 1 and j < coord2[1] and coord2[1] < j + 1 ): if (i, j) not in CELLS: CELLS.append((i, j)) continue # traverse segment1 = [i, j, i + 1, j] if Geometry.isSegmentIntersects(segment1, segment2): if (i, j) not in CELLS: CELLS.append((i, j)) continue segment1 = [i, j, i, j + 1] if Geometry.isSegmentIntersects(segment1, segment2): if (i, j) not in CELLS: CELLS.append((i, j)) continue segment1 = [i, j + 1, i + 1, j + 1] if Geometry.isSegmentIntersects(segment1, segment2): if (i, j) not in CELLS: CELLS.append((i, j)) continue segment1 = [i + 1, j, i + 1, j + 1] if Geometry.isSegmentIntersects(segment1, segment2): if (i, j) not in CELLS: CELLS.append((i, j)) continue return CELLS
[docs] def save(self, filename): """TODO""" outfile = open(filename, "wb") pickle.dump(self, outfile) outfile.close()
[docs] def load(filename): """TODO""" infile = open(filename, "rb") index = pickle.load(infile) infile.close() return index