diff --git a/.gitignore b/.gitignore index 62a58a3..a274a93 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ doc/build/ .idea/ .coverage +.vscode diff --git a/.travis.yml b/.travis.yml index 2c1e3b4..c6ba2bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python python: - "2.7" - "3.4" + - "3.6" before_install: - sudo apt-get remove -y libgdal1 - sudo add-apt-repository -y ppa:ubuntugis/ubuntugis-unstable @@ -13,6 +14,6 @@ install: - pip install flake8 - pip install coveralls script: -- flake8 --max-line-length=90 --ignore=E128,E221,E241,E251,E272,E731,W503,E402 quantized_mesh_tile tests +- flake8 --max-line-length=90 --ignore=E128,E221,E241,E251,E402 quantized_mesh_tile tests - coverage run --source=quantized_mesh_tile setup.py test after_success: coveralls diff --git a/doc/source/conf.py b/doc/source/conf.py index e9d158a..7bc81ed 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -65,9 +65,9 @@ # built documents. # # The short X.Y version. -version = u'0.5' +version = u'0.6' # The full version, including alpha/beta/rc tags. -release = u'0.5' +release = u'0.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/source/index.rst b/doc/source/index.rst index 7c450f5..967ceb7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -59,9 +59,7 @@ We use flake8 to lint the project. Here are the rules we ignore. * E221: multiple spaces before operator * E241: multiple spaces after ':' * E251: multiple spaces around keyword/parameter equals -* E272: multiple spaces before keyword -* E731: do not assign a lambda expression, use a def -* W503: line break before binary operator +* E402: module level import not at top of file Vizualize a terrain tile ------------------------ diff --git a/quantized_mesh_tile/horizon_occlusion_point.py b/quantized_mesh_tile/horizon_occlusion_point.py index 2b70a95..28c49e6 100644 --- a/quantized_mesh_tile/horizon_occlusion_point.py +++ b/quantized_mesh_tile/horizon_occlusion_point.py @@ -40,11 +40,13 @@ def fromPoints(points, boundingSphere): raise Exception('Your list of points must contain at least 2 points') # Bring coordinates to ellipsoid scaled coordinates - scaleDown = lambda coord: [coord[0] * rX, coord[1] * rY, coord[2] * rZ] + def scaleDown(coord): + return [coord[0] * rX, coord[1] * rY, coord[2] * rZ] scaledPoints = list(map(scaleDown, points)) scaledSphereCenter = scaleDown(boundingSphere.center) - magnitude = lambda coord: computeMagnitude(coord, scaledSphereCenter) + def magnitude(coord): + return computeMagnitude(coord, scaledSphereCenter) magnitudes = list(map(magnitude, scaledPoints)) return c3d.multiplyByScalar(scaledSphereCenter, max(magnitudes)) diff --git a/quantized_mesh_tile/llh_ecef.py b/quantized_mesh_tile/llh_ecef.py index 55bd3f1..84ff408 100644 --- a/quantized_mesh_tile/llh_ecef.py +++ b/quantized_mesh_tile/llh_ecef.py @@ -24,7 +24,9 @@ def LLH2ECEF(lon, lat, alt): lat *= (old_div(math.pi, 180.0)) lon *= (old_div(math.pi, 180.0)) - n = lambda x: old_div(wgs84_a, math.sqrt(1 - wgs84_e2 * (math.sin(x) ** 2))) + def n(x): + return old_div(wgs84_a, math.sqrt( + 1 - wgs84_e2 * (math.sin(x) ** 2))) x = (n(lat) + alt) * math.cos(lat) * math.cos(lon) y = (n(lat) + alt) * math.cos(lat) * math.sin(lon) diff --git a/quantized_mesh_tile/terrain.py b/quantized_mesh_tile/terrain.py index 505d175..3b8abb5 100644 --- a/quantized_mesh_tile/terrain.py +++ b/quantized_mesh_tile/terrain.py @@ -11,7 +11,6 @@ from future import standard_library standard_library.install_aliases() -from builtins import next from builtins import map from past.builtins import xrange from builtins import object @@ -23,13 +22,12 @@ from . import horizon_occlusion_point as occ from .utils import ( octEncode, octDecode, zigZagDecode, zigZagEncode, - gzipFileObject, ungzipFileObject, unpackEntry, unpackIndices, + gzipFileObject, ungzipFileObject, unpackEntry, decodeIndices, packEntry, packIndices, encodeIndices ) from .bbsphere import BoundingSphere from .topology import TerrainTopology -MAX = 32767.0 # For a tile of 256px * 256px TILEPXS = 65536 @@ -131,7 +129,8 @@ class TerrainTile(object): ]) vertexData = OrderedDict([ - ['vertexCount', 'I'], # 4bytes -> determines the size of the 3 following arrays + # 4bytes -> determines the size of the 3 following arrays + ['vertexCount', 'I'], ['uVertexCount', 'H'], # 2bytes, unsigned short ['vVertexCount', 'H'], ['heightVertexCount', 'H'] @@ -182,6 +181,10 @@ class TerrainTile(object): BYTESPLIT = 65636 + # min and max quantized values for indices + MIN = 0.0 + MAX = 32767.0 + # Coordinates are given in lon/lat WSG84 def __init__(self, *args, **kwargs): self._west = kwargs.get('west', -1.0) @@ -197,10 +200,11 @@ def __init__(self, *args, **kwargs): # Extensions self.vLight = [] self.watermask = kwargs.get('watermask', []) - self.hasWatermask = kwargs.get('hasWatermask', bool(len(self.watermask) > 0)) + self.hasWatermask = kwargs.get( + 'hasWatermask', bool(len(self.watermask) > 0)) self.header = OrderedDict() - for k, v in TerrainTile.quantizedMeshHeader.items(): + for k in TerrainTile.quantizedMeshHeader.keys(): self.header[k] = 0.0 self.u = [] self.v = [] @@ -257,8 +261,8 @@ def getVerticesCoordinates(self): """ A method to retrieve the coordinates of the vertices in lon,lat,height. """ - coordinates = [] self._computeVerticesCoordinates() + coordinates = [] for i, lon in enumerate(self._longs): coordinates.append((lon, self._lats[i], self._heights[i])) return coordinates @@ -268,13 +272,15 @@ def getTrianglesCoordinates(self): A method to retrieve triplet of coordinates representing the triangles in lon,lat,height. """ - triangles = [] self._computeVerticesCoordinates() - indices = iter(self.indices) - for i in xrange(0, len(self.indices) - 1, 3): - vi1 = next(indices) - vi2 = next(indices) - vi3 = next(indices) + triangles = [] + nbTriangles = len(self.indices) + if nbTriangles % 3 != 0: + raise Exception('Corrupted tile') + for i in xrange(0, nbTriangles - 1, 3): + vi1 = self.indices[i] + vi2 = self.indices[i + 1] + vi3 = self.indices[i + 2] triangle = ( (self._longs[vi1], self._lats[vi1], @@ -287,8 +293,6 @@ def getTrianglesCoordinates(self): self._heights[vi3]) ) triangles.append(triangle) - if len(list(indices)) > 0: - raise Exception('Corrupted tile') return triangles def _computeVerticesCoordinates(self): @@ -297,15 +301,17 @@ def _computeVerticesCoordinates(self): """ if len(self._longs) == 0: for u in self.u: - self._longs.append(lerp(self._west, self._east, old_div(float(u), MAX))) + self._longs.append( + lerp(self._west, self._east, old_div(float(u), self.MAX))) for v in self.v: - self._lats.append(lerp(self._south, self._north, old_div(float(v), MAX))) + self._lats.append( + lerp(self._south, self._north, old_div(float(v), self.MAX))) for h in self.h: self._heights.append( lerp( self.header['minimumHeight'], self.header['maximumHeight'], - old_div(float(h), MAX) + old_div(float(h), self.MAX) ) ) @@ -333,26 +339,16 @@ def fromBytesIO(self, f, hasLighting=False, hasWatermask=False): for k, v in TerrainTile.quantizedMeshHeader.items(): self.header[k] = unpackEntry(f, v) - # Delta decoding - ud = 0 - vd = 0 - hd = 0 # Vertices vertexCount = unpackEntry(f, TerrainTile.vertexData['vertexCount']) - for i in xrange(0, vertexCount): - ud += zigZagDecode( - unpackEntry(f, TerrainTile.vertexData['uVertexCount']) - ) + for ud in self._iterUnpackAndDecodeVertices( + f, vertexCount, TerrainTile.vertexData['uVertexCount']): self.u.append(ud) - for i in xrange(0, vertexCount): - vd += zigZagDecode( - unpackEntry(f, TerrainTile.vertexData['vVertexCount']) - ) + for vd in self._iterUnpackAndDecodeVertices( + f, vertexCount, TerrainTile.vertexData['vVertexCount']): self.v.append(vd) - for i in xrange(0, vertexCount): - hd += zigZagDecode( - unpackEntry(f, TerrainTile.vertexData['heightVertexCount']) - ) + for hd in self._iterUnpackAndDecodeVertices( + f, vertexCount, TerrainTile.vertexData['heightVertexCount']): self.h.append(hd) # Indices @@ -360,25 +356,30 @@ def fromBytesIO(self, f, hasLighting=False, hasWatermask=False): if vertexCount > TerrainTile.BYTESPLIT: meta = TerrainTile.indexData32 triangleCount = unpackEntry(f, meta['triangleCount']) - ind = unpackIndices(f, triangleCount * 3, meta['indices']) + ind = [ + index for index + in self._iterUnpackIndices(f, triangleCount * 3, meta['indices'])] self.indices = decodeIndices(ind) meta = TerrainTile.EdgeIndices16 if vertexCount > TerrainTile.BYTESPLIT: meta = TerrainTile.indexData32 # Edges (vertices on the edge of the tile) - # Indices (are the also high water mark encoded?) westIndicesCount = unpackEntry(f, meta['westVertexCount']) - self.westI = unpackIndices(f, westIndicesCount, meta['westIndices']) + for wi in self._iterUnpackIndices(f, westIndicesCount, meta['westIndices']): + self.westI.append(wi) southIndicesCount = unpackEntry(f, meta['southVertexCount']) - self.southI = unpackIndices(f, southIndicesCount, meta['southIndices']) + for si in self._iterUnpackIndices(f, southIndicesCount, meta['southIndices']): + self.southI.append(si) eastIndicesCount = unpackEntry(f, meta['eastVertexCount']) - self.eastI = unpackIndices(f, eastIndicesCount, meta['eastIndices']) + for ei in self._iterUnpackIndices(f, eastIndicesCount, meta['eastIndices']): + self.eastI.append(ei) northIndicesCount = unpackEntry(f, meta['northVertexCount']) - self.northI = unpackIndices(f, northIndicesCount, meta['northIndices']) + for ni in self._iterUnpackIndices(f, northIndicesCount, meta['northIndices']): + self.northI.append(ni) if self.hasLighting: # One byte of padding @@ -388,29 +389,78 @@ def fromBytesIO(self, f, hasLighting=False, hasWatermask=False): if extensionId == 1: extensionLength = unpackEntry(f, meta['extensionLength']) - for i in range(0, old_div(extensionLength, 2)): - x = unpackEntry(f, TerrainTile.OctEncodedVertexNormals['xy']) - y = unpackEntry(f, TerrainTile.OctEncodedVertexNormals['xy']) - self.vLight.append(octDecode(x, y)) + for xy in self._iterUnpackAndDecodeLight( + f, extensionLength, TerrainTile.OctEncodedVertexNormals['xy']): + self.vLight.append(xy) if self.hasWatermask: meta = TerrainTile.ExtensionHeader extensionId = unpackEntry(f, meta['extensionId']) if extensionId == 2: extensionLength = unpackEntry(f, meta['extensionLength']) - row = [] - for i in range(0, extensionLength): - row.append(unpackEntry(f, TerrainTile.WaterMask['xy'])) - if len(row) == 256: - self.watermask.append(row) - row = [] - if len(row) > 0: + for row in self._iterUnpackWatermaskRow( + f, extensionLength, TerrainTile.WaterMask['xy']): self.watermask.append(row) data = f.read(1) if data: raise Exception('Should have reached end of file, but didn\'t') + def _iterUnpackAndDecodeVertices(self, f, vertexCount, structType): + """ + A private method to itertatively unpack and decode indices. + """ + i = 0 + # Delta decoding + delta = 0 + while i != vertexCount: + delta += zigZagDecode(unpackEntry(f, structType)) + yield delta + i += 1 + + def _iterUnpackIndices(self, f, indicesCount, structType): + """ + A private method to iteratively unpack indices + """ + i = 0 + while i != indicesCount: + yield unpackEntry(f, structType) + i += 1 + + def _iterUnpackAndDecodeLight(self, f, extensionLength, structType): + """ + A private method to iteratively unpack light vector. + """ + i = 0 + xyCount = old_div(extensionLength, 2) + while i != xyCount: + yield octDecode( + unpackEntry( + f, structType), + unpackEntry( + f, structType) + ) + i += 1 + + def _iterUnpackWatermaskRow(self, f, extensionLength, structType): + """ + A private method to iteratively unpack watermask rows + """ + i = 0 + xyCount = 0 + row = [] + while xyCount != extensionLength: + row.append(unpackEntry(f, structType)) + if i == 255: + yield row + i = 0 + row = [] + else: + i += 1 + xyCount += 1 + if len(row) > 0: + yield row + def fromFile(self, filePath, hasLighting=False, hasWatermask=False, gzipped=False): """ A method to read a terrain tile file. It is assumed that the tile unzipped. @@ -436,7 +486,8 @@ def fromFile(self, filePath, hasLighting=False, hasWatermask=False, gzipped=Fals with open(filePath, 'rb') as f: if gzipped: f = ungzipFileObject(f) - self.fromBytesIO(f, hasLighting=hasLighting, hasWatermask=hasWatermask) + self.fromBytesIO(f, hasLighting=hasLighting, + hasWatermask=hasWatermask) def toBytesIO(self, gzipped=False): """ @@ -492,24 +543,30 @@ def _writeTo(self, f): f.write(packEntry(TerrainTile.vertexData['vertexCount'], vertexCount)) # Move the initial value f.write( - packEntry(TerrainTile.vertexData['uVertexCount'], zigZagEncode(self.u[0])) + packEntry( + TerrainTile.vertexData['uVertexCount'], zigZagEncode(self.u[0])) ) for i in xrange(0, vertexCount - 1): ud = self.u[i + 1] - self.u[i] - f.write(packEntry(TerrainTile.vertexData['uVertexCount'], zigZagEncode(ud))) + f.write( + packEntry(TerrainTile.vertexData['uVertexCount'], zigZagEncode(ud))) f.write( - packEntry(TerrainTile.vertexData['uVertexCount'], zigZagEncode(self.v[0])) + packEntry( + TerrainTile.vertexData['uVertexCount'], zigZagEncode(self.v[0])) ) for i in xrange(0, vertexCount - 1): vd = self.v[i + 1] - self.v[i] - f.write(packEntry(TerrainTile.vertexData['vVertexCount'], zigZagEncode(vd))) + f.write( + packEntry(TerrainTile.vertexData['vVertexCount'], zigZagEncode(vd))) f.write( - packEntry(TerrainTile.vertexData['uVertexCount'], zigZagEncode(self.h[0])) + packEntry( + TerrainTile.vertexData['uVertexCount'], zigZagEncode(self.h[0])) ) for i in xrange(0, vertexCount - 1): hd = self.h[i + 1] - self.h[i] f.write( - packEntry(TerrainTile.vertexData['heightVertexCount'], zigZagEncode(hd)) + packEntry( + TerrainTile.vertexData['heightVertexCount'], zigZagEncode(hd)) ) # Indices @@ -517,7 +574,8 @@ def _writeTo(self, f): if vertexCount > TerrainTile.BYTESPLIT: meta = TerrainTile.indexData32 - f.write(packEntry(meta['triangleCount'], old_div(len(self.indices), 3))) + f.write(packEntry(meta['triangleCount'], + old_div(len(self.indices), 3))) ind = encodeIndices(self.indices) packIndices(f, meta['indices'], ind) @@ -575,7 +633,8 @@ def _writeTo(self, f): x = self.watermask[i] if len(x) != 256: raise Exception( - 'Unexpected number of columns for the watermask: %s' % len(x) + 'Unexpected number of columns for the watermask: %s' % len( + x) ) # From West to East for y in x: @@ -584,7 +643,8 @@ def _writeTo(self, f): f.write(packEntry(meta['extensionLength'], 1)) if self.watermask[0][0] is None: self.watermask[0][0] = 0 - f.write(packEntry(TerrainTile.WaterMask['xy'], int(self.watermask[0][0]))) + f.write( + packEntry(TerrainTile.WaterMask['xy'], int(self.watermask[0][0]))) def fromTerrainTopology(self, topology, bounds=None): """ @@ -606,7 +666,8 @@ def fromTerrainTopology(self, topology, bounds=None): """ if not isinstance(topology, TerrainTopology): - raise Exception('topology object must be an instance of TerrainTopology') + raise Exception( + 'topology object must be an instance of TerrainTopology') # If the bounds are not provided use # topology extent instead @@ -636,7 +697,7 @@ def fromTerrainTopology(self, topology, bounds=None): ecefMaxY = topology.ecefMaxY ecefMaxZ = topology.ecefMaxZ - # Center of the bounding box 3d (TODO verify) + # Center of the bounding box 3d centerCoords = [ ecefMinX + (ecefMaxX - ecefMinX) * 0.5, ecefMinY + (ecefMaxY - ecefMinY) * 0.5, @@ -645,7 +706,7 @@ def fromTerrainTopology(self, topology, bounds=None): occlusionPCoords = occ.fromPoints(topology.cartesianVertices, bSphere) - for k, v in TerrainTile.quantizedMeshHeader.items(): + for k in TerrainTile.quantizedMeshHeader.keys(): if k == 'centerX': self.header[k] = centerCoords[0] elif k == 'centerY': @@ -671,21 +732,26 @@ def fromTerrainTopology(self, topology, bounds=None): elif k == 'horizonOcclusionPointZ': self.header[k] = occlusionPCoords[2] - bLon = old_div(MAX, (self._east - self._west)) - bLat = old_div(MAX, (self._north - self._south)) + bLon = old_div(self.MAX, (self._east - self._west)) + bLat = old_div(self.MAX, (self._north - self._south)) - quantizeLonIndices = lambda x: int(round((x - self._west) * bLon)) - quantizeLatIndices = lambda x: int(round((x - self._south) * bLat)) + def quantizeLonIndices(x): + return int(round((x - self._west) * bLon)) + + def quantizeLatIndices(x): + return int(round((x - self._south) * bLat)) deniv = self.header['maximumHeight'] - self.header['minimumHeight'] # In case a tile is completely flat if deniv == 0: - quantizeHeightIndices = lambda x: 0 + def quantizeHeightIndices(x): + return 0 else: - bHeight = old_div(MAX, deniv) - quantizeHeightIndices = lambda x: int( - round((x - self.header['minimumHeight']) * bHeight) - ) + bHeight = old_div(self.MAX, deniv) + + def quantizeHeightIndices(x): + return int( + round((x - self.header['minimumHeight']) * bHeight)) # High watermark encoding performed during toFile self.u = list(map(quantizeLonIndices, topology.uVertex)) @@ -694,21 +760,19 @@ def fromTerrainTopology(self, topology, bounds=None): self.indices = topology.indexData # List all the vertices on the edge of the tile - # High water mark encoded? - for i in range(0, len(self.indices)): - # Use original coordinates - indice = self.indices[i] - lon = topology.uVertex[indice] - lat = topology.vVertex[indice] - - if lon == self._west and indice not in self.westI: + # Use quantized values to determine if an indice belong to a tile edge + for indice in self.indices: + x = self.u[indice] + y = self.v[indice] + + if x == self.MIN and indice not in self.westI: self.westI.append(indice) - elif lon == self._east and indice not in self.eastI: + elif x == self.MAX and indice not in self.eastI: self.eastI.append(indice) - if lat == self._south and indice not in self.southI: + if y == self.MIN and indice not in self.southI: self.southI.append(indice) - elif lat == self._north and indice not in self.northI: + elif y == self.MAX and indice not in self.northI: self.northI.append(indice) self.hasLighting = topology.hasLighting diff --git a/quantized_mesh_tile/utils.py b/quantized_mesh_tile/utils.py index 4730bd3..f5db376 100644 --- a/quantized_mesh_tile/utils.py +++ b/quantized_mesh_tile/utils.py @@ -30,15 +30,6 @@ def packIndices(f, type, indices): f.write(packEntry(type, i)) -def unpackIndices(f, indicesCount, indicesType): - indices = [] - for i in xrange(0, indicesCount): - indices.append( - unpackEntry(f, indicesType) - ) - return indices - - def decodeIndices(indices): out = [] highest = 0 diff --git a/setup.py b/setup.py index 85df2e4..588589c 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ requires = ['shapely', 'numpy'] setup(name='quantized-mesh-tile', - version='0.5', + version='0.6', description='Quantized-Mesh format reader and writer', author=u'Loic Gasser', author_email='loicgasser4@gmail.com', diff --git a/tests/test_bbsphere.py b/tests/test_bbsphere.py index c9bc9d2..28d3356 100644 --- a/tests/test_bbsphere.py +++ b/tests/test_bbsphere.py @@ -73,7 +73,8 @@ def testBoundingSpherePrecision(self): ter = TerrainTile(west=minx, south=miny, east=maxx, north=maxy) ter.fromFile('tests/data/%s_%s_%s.terrain' % (z, x, y)) - llh2ecef = lambda x: LLH2ECEF(x[0], x[1], x[2]) + def llh2ecef(x): + return LLH2ECEF(x[0], x[1], x[2]) coords = ter.getVerticesCoordinates() coords = list(map(llh2ecef, coords)) sphere = BoundingSphere() diff --git a/tests/test_terrain_tile.py b/tests/test_terrain_tile.py index b62edf3..bddc385 100644 --- a/tests/test_terrain_tile.py +++ b/tests/test_terrain_tile.py @@ -95,6 +95,13 @@ def testReaderWriter(self): for i, v in enumerate(ter.northI): self.assertEqual(v, ter2.northI[i], i) + # check vertice sitting on the edge + # we already know the number of indices on the edges + self.assertEqual(len(ter2.westI), 30) + self.assertEqual(len(ter2.eastI), 10) + self.assertEqual(len(ter2.southI), 14) + self.assertEqual(len(ter2.northI), 25) + self.assertEqual(ter2.getContentType(), 'application/vnd.quantized-mesh') @@ -167,13 +174,15 @@ def testExtensionsReader(self): self.assertEqual(len(ter.watermask), len(ter2.watermask)) self.assertEqual(len(ter.watermask[0]), len(ter2.watermask[0])) - sign = lambda a: 1 if a > 0 else -1 if a < 0 else 0 + def sign(a): + return 1 if a > 0 else -1 if a < 0 else 0 for i in range(0, len(ter.vLight)): for j in range(0, 3): # We cannot have an exact equality with successive # oct encoding and decoding # Thus we only check the sign - self.assertEqual(sign(ter.vLight[i][j]), sign(ter2.vLight[i][j])) + self.assertEqual( + sign(ter.vLight[i][j]), sign(ter2.vLight[i][j])) self.assertEqual(ter2.getContentType(), 'application/vnd.quantized-mesh;' + @@ -238,6 +247,11 @@ def testTileCreationFromTopology(self): self.assertEqual(tile._east, 1.0) self.assertEqual(tile._north, 1.0) + self.assertEqual(len(tile.westI), 2) + self.assertEqual(len(tile.eastI), 2) + self.assertEqual(len(tile.southI), 2) + self.assertEqual(len(tile.northI), 2) + fileLike = tile.toBytesIO() self.assertIsInstance(fileLike, io.BytesIO) @@ -255,6 +269,11 @@ def testGzippedTileCreationFromTopology(self): self.assertEqual(tile._east, 1.0) self.assertEqual(tile._north, 1.0) + self.assertEqual(len(tile.westI), 2) + self.assertEqual(len(tile.eastI), 2) + self.assertEqual(len(tile.southI), 2) + self.assertEqual(len(tile.northI), 2) + fileLike = tile.toBytesIO(gzipped=True) self.assertIsInstance(fileLike, io.BytesIO)