From d318d4eaf892f4226a48f442fc2a6364f2c65513 Mon Sep 17 00:00:00 2001 From: Oscar Lorentzon Date: Sat, 27 Mar 2021 09:52:09 +0100 Subject: [PATCH] refactor: remove redundant lat lon to cell ids provider method --- spec/api/GeohashGeometryProvider.spec.ts | 276 +++++++---------------- spec/api/GeometryProviderBase.spec.ts | 217 ++++++++++++++++++ spec/api/S2GeometryProvider.spec.ts | 259 +++++++++++---------- spec/graph/Graph.spec.ts | 54 +++-- src/api/GeohashGeometryProvider.ts | 34 +-- src/api/GeometryProviderBase.ts | 64 +++--- src/api/S2GeometryProvider.ts | 20 +- src/graph/Graph.ts | 166 +++++++------- 8 files changed, 579 insertions(+), 511 deletions(-) create mode 100644 spec/api/GeometryProviderBase.spec.ts diff --git a/spec/api/GeohashGeometryProvider.spec.ts b/spec/api/GeohashGeometryProvider.spec.ts index b85bbeae6..2b62cce56 100644 --- a/spec/api/GeohashGeometryProvider.spec.ts +++ b/spec/api/GeohashGeometryProvider.spec.ts @@ -1,39 +1,32 @@ import * as geohash from "latlon-geohash"; import { GeohashGeometryProvider } from "../../src/api/GeohashGeometryProvider"; +import { LatLon } from "../../src/api/interfaces/LatLon"; import { MapillaryError } from "../../src/error/MapillaryError"; import * as GeoCoords from "../../src/geo/GeoCoords"; -jest.mock("latlon-geohash"); -const mockedGeohash = geohash as jest.Mocked; - describe("GeohashGeometryProvider.ctor", () => { test("should be defined", () => { const geometry = new GeohashGeometryProvider(); - expect(geometry).toBeDefined(); }); }); describe("GeohashGeometryProvider.latLonToCellId", () => { - beforeEach(() => { mockedGeohash.encode.mockClear(); }); - test("should call encoder correctly", () => { - const mockEncode = mockedGeohash.encode.mockReturnValueOnce("0/0"); - - const level = 7; - const geometry = new GeohashGeometryProvider(); + const mockEncode = spyOn(geohash, "encode").and.returnValue("0/0"); + const level = 22; + const geometry = new GeohashGeometryProvider(level); const lat = -1; const lon = 1; - - geometry.latLonToCellId({ lat: -1, lon: 1 }); + geometry.latLonToCellId({ lat, lon }); expect(mockEncode).toHaveBeenCalledTimes(1); expect(mockEncode).toHaveBeenCalledWith(lat, lon, level); }); }); -describe("GeohashGeometryProvider.latLonToCellIds", () => { +describe("GeohashGeometryProvider.bboxToCellIds", () => { const setupSpies: (tileSize: number) => void = (tileSize: number): void => { spyOn(geohash, "encode").and.callFake( @@ -75,6 +68,20 @@ describe("GeohashGeometryProvider.latLonToCellIds", () => { }; }); + spyOn(GeoCoords, "geodeticToEnu").and.callFake( + ( + lat: number, + lon: number, + _: number, + refLat: number, + refLon: number) + : number[] => { + return [ + tileSize * (lat - refLat), + tileSize * (lon - refLon), + 0]; + }); + spyOn(GeoCoords, "enuToGeodetic").and.callFake( (x: number, y: number, _: number, refLat: number, refLon: number): number[] => { return [ @@ -84,168 +91,38 @@ describe("GeohashGeometryProvider.latLonToCellIds", () => { }); }; - test("should return h of position only", () => { + test("should return cell", () => { const geometry = new GeohashGeometryProvider(); - - const threshold = 20; - const tileSize = 2 * (threshold + 1); - + const tileSize = 1; setupSpies(tileSize); - const hs = geometry.latLonToCellIds({ lat: 0, lon: 0 }, threshold); + const sw: LatLon = { lat: -0.1, lon: -0.1 }; + const ne: LatLon = { lat: 0.1, lon: 0.1 }; + const cellIds = geometry.bboxToCellIds(sw, ne); - expect(hs.length).toBe(1); - expect(hs[0]).toBe("0/0"); + expect(cellIds.length).toBe(1); + expect(cellIds[0]).toBe("0/0"); }); - test("should return h of position and north neighbour", () => { + test("should return cell and adjacent", () => { const geometry = new GeohashGeometryProvider(); - - const threshold = 20; - const tileSize = 2 * (threshold + 1); - + const tileSize = 1; setupSpies(tileSize); - const hs = geometry - .latLonToCellIds( - { lat: 0, lon: 0.4 }, - threshold); - - expect(hs.length).toBe(9); - expect(hs.indexOf("0/0")).not.toBe(-1); - expect(hs.indexOf("0/1")).not.toBe(-1); - expect(hs.indexOf("0/-1")).not.toBe(-1); - expect(hs.indexOf("1/0")).not.toBe(-1); - expect(hs.indexOf("1/-1")).not.toBe(-1); - expect(hs.indexOf("1/1")).not.toBe(-1); - expect(hs.indexOf("-1/0")).not.toBe(-1); - expect(hs.indexOf("-1/-1")).not.toBe(-1); - expect(hs.indexOf("-1/1")).not.toBe(-1); - }); - - test("should return neighbors", () => { - const geometry = new GeohashGeometryProvider(); - - const threshold = 20; - const tileSize = 2 * (threshold + 1); - - setupSpies(tileSize); - - const hs = geometry - .latLonToCellIds( - { lat: 0.4, lon: 0 }, - threshold); - - expect(hs.length).toBe(9); - expect(hs.indexOf("0/0")).not.toBe(-1); - expect(hs.indexOf("0/1")).not.toBe(-1); - expect(hs.indexOf("0/-1")).not.toBe(-1); - expect(hs.indexOf("1/0")).not.toBe(-1); - expect(hs.indexOf("1/-1")).not.toBe(-1); - expect(hs.indexOf("1/1")).not.toBe(-1); - expect(hs.indexOf("-1/0")).not.toBe(-1); - expect(hs.indexOf("-1/-1")).not.toBe(-1); - expect(hs.indexOf("-1/1")).not.toBe(-1); - }); - - test("should return neighbors", () => { - const geometry = new GeohashGeometryProvider(); - - const threshold = 20; - const tileSize = 2 * (threshold + 1); - - setupSpies(tileSize); - - const hs = geometry - .latLonToCellIds( - { lat: 0, lon: -0.4 }, - threshold); - - expect(hs.length).toBe(9); - expect(hs.indexOf("0/0")).not.toBe(-1); - expect(hs.indexOf("0/1")).not.toBe(-1); - expect(hs.indexOf("0/-1")).not.toBe(-1); - expect(hs.indexOf("1/0")).not.toBe(-1); - expect(hs.indexOf("1/-1")).not.toBe(-1); - expect(hs.indexOf("1/1")).not.toBe(-1); - expect(hs.indexOf("-1/0")).not.toBe(-1); - expect(hs.indexOf("-1/-1")).not.toBe(-1); - expect(hs.indexOf("-1/1")).not.toBe(-1); - }); - - test("should return neighbours", () => { - const geometry = new GeohashGeometryProvider(); - - const threshold = 20; - const tileSize = 2 * (threshold + 1); - - setupSpies(tileSize); - - const hs = geometry - .latLonToCellIds( - { lat: -0.4, lon: 0 }, - threshold); - - expect(hs.length).toBe(9); - expect(hs.indexOf("0/0")).not.toBe(-1); - expect(hs.indexOf("0/1")).not.toBe(-1); - expect(hs.indexOf("0/-1")).not.toBe(-1); - expect(hs.indexOf("1/0")).not.toBe(-1); - expect(hs.indexOf("1/-1")).not.toBe(-1); - expect(hs.indexOf("1/1")).not.toBe(-1); - expect(hs.indexOf("-1/0")).not.toBe(-1); - expect(hs.indexOf("-1/-1")).not.toBe(-1); - expect(hs.indexOf("-1/1")).not.toBe(-1); - }); - - test("should return h of position and north east neighbours", () => { - const geometry = new GeohashGeometryProvider(); - - const threshold = 20; - const tileSize = 2 * (threshold + 1); - - setupSpies(tileSize); - - const hs = geometry - .latLonToCellIds( - { lat: 0.4, lon: 0.4 }, - threshold); - - expect(hs.length).toBe(9); - expect(hs.indexOf("0/0")).not.toBe(-1); - expect(hs.indexOf("0/1")).not.toBe(-1); - expect(hs.indexOf("0/-1")).not.toBe(-1); - expect(hs.indexOf("1/0")).not.toBe(-1); - expect(hs.indexOf("1/-1")).not.toBe(-1); - expect(hs.indexOf("1/1")).not.toBe(-1); - expect(hs.indexOf("-1/0")).not.toBe(-1); - expect(hs.indexOf("-1/-1")).not.toBe(-1); - expect(hs.indexOf("-1/1")).not.toBe(-1); - }); - - test("should return h of position and all neighbours", () => { - const geometry = new GeohashGeometryProvider(); - - const threshold = 20; - const tileSize = 2 * (threshold - 1); - - setupSpies(tileSize); - - const hs = geometry - .latLonToCellIds( - { lat: 0, lon: 0 }, - threshold); - - expect(hs.length).toBe(9); - expect(hs.indexOf("0/0")).not.toBe(-1); - expect(hs.indexOf("0/1")).not.toBe(-1); - expect(hs.indexOf("-1/1")).not.toBe(-1); - expect(hs.indexOf("-1/0")).not.toBe(-1); - expect(hs.indexOf("-1/-1")).not.toBe(-1); - expect(hs.indexOf("0/-1")).not.toBe(-1); - expect(hs.indexOf("1/-1")).not.toBe(-1); - expect(hs.indexOf("1/0")).not.toBe(-1); - expect(hs.indexOf("1/1")).not.toBe(-1); + const sw: LatLon = { lat: -0.6, lon: -0.6 }; + const ne: LatLon = { lat: 0.6, lon: 0.6 }; + const cellIds = geometry.bboxToCellIds(sw, ne); + + expect(cellIds.length).toBe(9); + expect(cellIds.indexOf("0/0")).not.toBe(-1); + expect(cellIds.indexOf("0/1")).not.toBe(-1); + expect(cellIds.indexOf("0/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/0")).not.toBe(-1); + expect(cellIds.indexOf("1/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/1")).not.toBe(-1); + expect(cellIds.indexOf("-1/0")).not.toBe(-1); + expect(cellIds.indexOf("-1/-1")).not.toBe(-1); + expect(cellIds.indexOf("-1/1")).not.toBe(-1); }); }); @@ -254,37 +131,52 @@ describe("GeohashGeometryProvider.bboxToCellIds", () => { const geometry = new GeohashGeometryProvider(); expect(() => { - geometry.bboxToCellIds( - { lat: 0, lon: 0 }, - { lat: -1, lon: 1 }); - }) - .toThrowError(MapillaryError); + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: -1, lon: 1 }); + }).toThrowError(MapillaryError); + expect(() => { - geometry.bboxToCellIds( - { lat: 0, lon: 0 }, - { lat: 1, lon: -1 }); - }) - .toThrowError(MapillaryError); + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: 1, lon: -1 }); + }).toThrowError(MapillaryError); + expect(() => { - geometry.bboxToCellIds( - { lat: 0, lon: 0 }, - { lat: -1, lon: -1 }); - }) - .toThrowError(MapillaryError); + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: -1, lon: -1 }); + }).toThrowError(MapillaryError); }); - test( - "should call latLonToCellIds with center and correct threshold", - () => { + describe("GeohashGeometryProvider.getAdjacent", () => { + it("should always be 8", () => { const geometry = new GeohashGeometryProvider(); - spyOn(GeoCoords, "geodeticToEnu").and.returnValue([10, 20, 0]); - const encodeHsSpy = spyOn(geometry, "latLonToCellIds").and.stub(); - - geometry.bboxToCellIds({ lat: 0, lon: 0 }, { lat: 1, lon: 3 }); - - expect(encodeHsSpy.calls.count()).toBe(1); - expect(encodeHsSpy.calls.argsFor(0)[0].lat).toBe(0.5); - expect(encodeHsSpy.calls.argsFor(0)[0].lon).toBe(1.5); + const latLons: LatLon[] = [ + { lat: 45, lon: 0 }, + { lat: 0, lon: 45 }, + { lat: -45, lon: 0 }, + { lat: 0, lon: -45 }, + { lat: 45, lon: 45 }, + { lat: -45, lon: -45 }, + { lat: 45, lon: -45 }, + { lat: -45, lon: 45 }, + { lat: -45, lon: 135 }, + { lat: -45, lon: 180 }, + { lat: 0, lon: 180 }, + { lat: 45, lon: 180 }, + ]; + + for (let latLon of latLons) { + console.log(latLon) + const cellId = geometry.latLonToCellId(latLon); + const adjacent = geometry.getAdjacent(cellId); + expect(adjacent.length).toBe(8); + } }); + }); }); diff --git a/spec/api/GeometryProviderBase.spec.ts b/spec/api/GeometryProviderBase.spec.ts new file mode 100644 index 000000000..91489b379 --- /dev/null +++ b/spec/api/GeometryProviderBase.spec.ts @@ -0,0 +1,217 @@ +import { LatLon } from "../../src/api/interfaces/LatLon"; +import { MapillaryError } from "../../src/error/MapillaryError"; +import { GeometryProviderBase } from "../../src/export/APINamespace"; +import * as GeoCoords from "../../src/geo/GeoCoords"; +import { isClockwise } from "../helper/TestMath"; + +class GeometryProvider extends GeometryProviderBase { + public bboxToCellIds(sw: LatLon, ne: LatLon): string[] { + return this._approxBboxToCellIds(sw, ne); + } + + public getVertices(cellId: string): LatLon[] { + const ll = this._cellIdToLatLng(cellId); + const lat = ll.lat; + const lon = ll.lon; + return [ + { lat: lat - 0.5, lon: lon + 0.5 }, + { lat: lat - 0.5, lon: lon - 0.5 }, + { lat: lat + 0.5, lon: lon - 0.5 }, + { lat: lat + 0.5, lon: lon + 0.5 }, + ]; + } + + public getAdjacent(cellId: string): string[] { + const ll = this._cellIdToLatLng(cellId); + const lat = ll.lat; + const lon = ll.lon; + return [ + `${lat}/${lon - 1}`, + `${lat + 1}/${lon}`, + `${lat}/${lon + 1}`, + `${lat - 1}/${lon}`, + `${lat + 1}/${lon - 1}`, + `${lat + 1}/${lon + 1}`, + `${lat - 1}/${lon - 1}`, + `${lat - 1}/${lon + 1}`, + ]; + } + + public latLonToCellId(latLon: LatLon): string { + const lat = latLon.lat; + const lon = latLon.lon; + return `${Math.round(lat)}/${Math.round(lon)}`; + } + + private _cellIdToLatLng(cellId: string): LatLon { + const [c0, c1] = cellId.split("/"); + const lat = Number.parseInt(c0, 10); + const lon = Number.parseInt(c1, 10); + return { lat, lon }; + } +} + +describe("GeometryProvider.ctor", () => { + it("should be defined", () => { + const geometry = new GeometryProvider(); + expect(geometry).toBeDefined(); + }); +}); + +describe("GeometryProvider.latLonToCellId", () => { + it("should call geometry correctly", () => { + const geometry = new GeometryProvider(); + const cellId = geometry.latLonToCellId({ lat: 0, lon: 0 }); + expect(cellId).toBe("0/0"); + }); +}); + +describe("GeometryProvider.bboxToCellIds", () => { + beforeEach(() => { + spyOn(GeoCoords, "geodeticToEnu").and.callFake( + ( + lat: number, + lon: number, + _: number, + refLat: number, + refLon: number) + : number[] => { + return [ + refLon + lon, + refLat + lat, + 0]; + }); + + spyOn(GeoCoords, "enuToGeodetic").and.callFake( + (x: number, y: number, _: number, refLat: number, refLon: number): number[] => { + return [ + refLon + x, + refLat + y, + 0]; + }); + }) + + it("should return cell", () => { + const geometry = new GeometryProvider(); + const sw: LatLon = { lat: -0.1, lon: -0.1 }; + const ne: LatLon = { lat: 0.1, lon: 0.1 }; + const cellIds = geometry.bboxToCellIds(sw, ne); + + expect(cellIds.length).toBe(1); + expect(cellIds[0]).toBe("0/0"); + }); + + it("should return cell and adjacent", () => { + const geometry = new GeometryProvider(); + const sw: LatLon = { lat: -0.6, lon: -0.6 }; + const ne: LatLon = { lat: 0.6, lon: 0.6 }; + const cellIds = geometry.bboxToCellIds(sw, ne); + + expect(cellIds.length).toBe(9); + expect(cellIds.indexOf("0/0")).not.toBe(-1); + expect(cellIds.indexOf("0/1")).not.toBe(-1); + expect(cellIds.indexOf("0/-1")).not.toBe(-1); + expect(cellIds.indexOf("-1/1")).not.toBe(-1); + expect(cellIds.indexOf("-1/0")).not.toBe(-1); + expect(cellIds.indexOf("-1/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/0")).not.toBe(-1); + expect(cellIds.indexOf("1/1")).not.toBe(-1); + }); + + it("should return cell and adjacent", () => { + const geometry = new GeometryProvider(); + const sw: LatLon = { lat: -0.1, lon: -0.1 }; + const ne: LatLon = { lat: 0.1, lon: 0.6 }; + const cellIds = geometry.bboxToCellIds(sw, ne); + + expect(cellIds.length).toBe(9); + expect(cellIds.indexOf("0/0")).not.toBe(-1); + expect(cellIds.indexOf("0/1")).not.toBe(-1); + expect(cellIds.indexOf("0/-1")).not.toBe(-1); + expect(cellIds.indexOf("-1/1")).not.toBe(-1); + expect(cellIds.indexOf("-1/0")).not.toBe(-1); + expect(cellIds.indexOf("-1/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/0")).not.toBe(-1); + expect(cellIds.indexOf("1/1")).not.toBe(-1); + }); + + it("should return cell and adjacent", () => { + const geometry = new GeometryProvider(); + const sw: LatLon = { lat: -0.1, lon: -0.1 }; + const ne: LatLon = { lat: 0.6, lon: 0.1 }; + const cellIds = geometry.bboxToCellIds(sw, ne); + + expect(cellIds.length).toBe(9); + expect(cellIds.indexOf("0/0")).not.toBe(-1); + expect(cellIds.indexOf("0/1")).not.toBe(-1); + expect(cellIds.indexOf("0/-1")).not.toBe(-1); + expect(cellIds.indexOf("-1/1")).not.toBe(-1); + expect(cellIds.indexOf("-1/0")).not.toBe(-1); + expect(cellIds.indexOf("-1/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/-1")).not.toBe(-1); + expect(cellIds.indexOf("1/0")).not.toBe(-1); + expect(cellIds.indexOf("1/1")).not.toBe(-1); + }); +}); + +describe("S2GeometryProvider.bboxToCellIds", () => { + it("should throw if north east is not larger than south west", () => { + const geometry = new GeometryProvider(); + + expect(() => { + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: -1, lon: 1 }); + }).toThrowError(MapillaryError); + + expect(() => { + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: 1, lon: -1 }); + }).toThrowError(MapillaryError); + + expect(() => { + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: -1, lon: -1 }); + }).toThrowError(MapillaryError); + }); +}); + +describe("S2GeometryProvider.getCorners", () => { + it("should be correctly placed relative to each other", () => { + const geometry = new GeometryProvider(); + + const latLons: LatLon[] = [ + { lat: 0, lon: 0 }, + { lat: 45, lon: 0 }, + { lat: 0, lon: 45 }, + { lat: -45, lon: 0 }, + { lat: 0, lon: -45 }, + { lat: 45, lon: 45 }, + { lat: -45, lon: -45 }, + { lat: 45, lon: -45 }, + { lat: -45, lon: 45 }, + { lat: -45, lon: 135 }, + ]; + + for (let latLon of latLons) { + const cellId = geometry.latLonToCellId(latLon); + const vertices = geometry.getVertices(cellId); + expect(vertices.length).toBe(4); + + const polygon = vertices + .map( + (ll: LatLon): number[] => { + return [ll.lon, ll.lat]; + }); + + expect(isClockwise(polygon)).toBe(true); + } + }); +}); diff --git a/spec/api/S2GeometryProvider.spec.ts b/spec/api/S2GeometryProvider.spec.ts index 8db8e4cda..9db85d025 100644 --- a/spec/api/S2GeometryProvider.spec.ts +++ b/spec/api/S2GeometryProvider.spec.ts @@ -7,7 +7,7 @@ import { isClockwise } from "../helper/TestMath"; describe("S2GeometryProvider.ctor", () => { it("should be defined", () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); + const geometry = new S2GeometryProvider(); expect(geometry).toBeDefined(); }); @@ -15,28 +15,29 @@ describe("S2GeometryProvider.ctor", () => { describe("GS2GeometryProvider.latLonToCellId", () => { it("should call geometry correctly", () => { - const keySpy: jasmine.Spy = spyOn(S2, "latLngToKey"); - const idSpy: jasmine.Spy = spyOn(S2, "keyToId"); + const keySpy = spyOn(S2, "latLngToKey"); + const idSpy = spyOn(S2, "keyToId"); keySpy.and.returnValue("0/0"); idSpy.and.returnValue("0.0"); - const geometry: S2GeometryProvider = new S2GeometryProvider(); + const level = 22; + const geometry = new S2GeometryProvider(level); - const lat: number = -1; - const lon: number = 1; - - geometry.latLonToCellId({ lat: -1, lon: 1 }); + const lat = -1; + const lon = 1; + geometry.latLonToCellId({ lat, lon }); expect(keySpy.calls.count()).toBe(1); expect(keySpy.calls.first().args[0]).toBe(lat); expect(keySpy.calls.first().args[1]).toBe(lon); + expect(keySpy.calls.first().args[2]).toBe(level); expect(idSpy.calls.count()).toBe(1); expect(idSpy.calls.first().args[0]).toBe("0/0"); }); }); -describe("S2GeometryProvider.latLonToCellIds", () => { +describe("S2GeometryProvider.bboxToCellIds", () => { const setupSpies: (tileSize: number) => void = (tileSize: number): void => { spyOn(S2, "latLngToKey").and.callFake( @@ -46,30 +47,30 @@ describe("S2GeometryProvider.latLonToCellIds", () => { spyOn(S2, "keyToId").and.callFake( (key: string): string => { - const [k0, k1]: string[] = key.split("/"); + const [k0, k1] = key.split("/"); return `${k0}.${k1}`; }); spyOn(S2, "idToKey").and.callFake( (id: string): string => { - const [i0, i1]: string[] = id.split("."); + const [i0, i1] = id.split("."); return `${i0}/${i1}`; }); spyOn(S2, "keyToLatLng").and.callFake( (key: string): S2.ILatLng => { - const [k0, k1]: string[] = key.split("/"); - const lat: number = Number.parseInt(k0, 10); - const lng: number = Number.parseInt(k1, 10); + const [k0, k1] = key.split("/"); + const lat = Number.parseInt(k0, 10); + const lng = Number.parseInt(k1, 10); return { lat: lat, lng: lng }; }); spyOn(S2.S2Cell, "FromHilbertQuadKey").and.callFake( (key: string): S2.S2Cell => { - const [k0, k1]: string[] = key.split("/"); - const lat: number = Number.parseInt(k0, 10); - const lng: number = Number.parseInt(k1, 10); + const [k0, k1] = key.split("/"); + const lat = Number.parseInt(k0, 10); + const lng = Number.parseInt(k1, 10); const s2Cell: S2.S2Cell = new S2.S2Cell(); spyOn(s2Cell, "getCornerLatLngs").and.returnValue([ @@ -92,6 +93,20 @@ describe("S2GeometryProvider.latLonToCellIds", () => { ]; }); + spyOn(GeoCoords, "geodeticToEnu").and.callFake( + ( + lat: number, + lon: number, + _: number, + refLat: number, + refLon: number) + : number[] => { + return [ + tileSize * (lon - refLon), + tileSize * (lat - refLat), + 0]; + }); + spyOn(GeoCoords, "enuToGeodetic").and.callFake( (x: number, y: number, _: number, refLat: number, refLon: number): number[] => { return [ @@ -101,78 +116,27 @@ describe("S2GeometryProvider.latLonToCellIds", () => { }); }; - it("should return cell id of position only", () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); - - const threshold: number = 20; - const tileSize: number = 2 * (threshold + 1); - + it("should return cell", () => { + const geometry = new S2GeometryProvider(); + const tileSize = 1; setupSpies(tileSize); - const cellIds: string[] = geometry.latLonToCellIds({ lat: 0, lon: 0 }, threshold); + const sw: LatLon = { lat: -0.1, lon: -0.1 }; + const ne: LatLon = { lat: 0.1, lon: 0.1 }; + const cellIds = geometry.bboxToCellIds(sw, ne); expect(cellIds.length).toBe(1); expect(cellIds[0]).toBe("0.0"); }); - it( - "should return cell id of position and all " + - "neighbours when tile outside", - () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); - - const threshold: number = 20; - const tileSize: number = 2 * (threshold - 1); - - setupSpies(tileSize); - - const cellIds: string[] = geometry.latLonToCellIds({ lat: 0, lon: 0 }, threshold); - - expect(cellIds.length).toBe(9); - expect(cellIds.indexOf("0.0")).not.toBe(-1); - expect(cellIds.indexOf("0.1")).not.toBe(-1); - expect(cellIds.indexOf("-1.1")).not.toBe(-1); - expect(cellIds.indexOf("-1.0")).not.toBe(-1); - expect(cellIds.indexOf("-1.-1")).not.toBe(-1); - expect(cellIds.indexOf("0.-1")).not.toBe(-1); - expect(cellIds.indexOf("1.-1")).not.toBe(-1); - expect(cellIds.indexOf("1.0")).not.toBe(-1); - expect(cellIds.indexOf("1.1")).not.toBe(-1); - }); - - it( - "should return cell id of position and all neighbours when outside", - () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); - - const threshold: number = 20; - const tileSize: number = 2 * (threshold + 1); - - setupSpies(tileSize); - - const cellIds: string[] = geometry.latLonToCellIds({ lat: 0, lon: 0.4 }, threshold); - - expect(cellIds.length).toBe(9); - expect(cellIds.indexOf("0.0")).not.toBe(-1); - expect(cellIds.indexOf("0.1")).not.toBe(-1); - expect(cellIds.indexOf("-1.1")).not.toBe(-1); - expect(cellIds.indexOf("-1.0")).not.toBe(-1); - expect(cellIds.indexOf("-1.-1")).not.toBe(-1); - expect(cellIds.indexOf("0.-1")).not.toBe(-1); - expect(cellIds.indexOf("1.-1")).not.toBe(-1); - expect(cellIds.indexOf("1.0")).not.toBe(-1); - expect(cellIds.indexOf("1.1")).not.toBe(-1); - }); - - it("should return cell id of position and all neighbours when outside", () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); - - const threshold: number = 20; - const tileSize: number = 2 * (threshold + 1); - + it("should return cell and adjacent", () => { + const geometry = new S2GeometryProvider(); + const tileSize = 1; setupSpies(tileSize); - const cellIds: string[] = geometry.latLonToCellIds({ lat: 0.4, lon: 0 }, threshold); + const sw: LatLon = { lat: -0.6, lon: -0.6 }; + const ne: LatLon = { lat: 0.6, lon: 0.6 }; + const cellIds = geometry.bboxToCellIds(sw, ne); expect(cellIds.length).toBe(9); expect(cellIds.indexOf("0.0")).not.toBe(-1); @@ -185,63 +149,92 @@ describe("S2GeometryProvider.latLonToCellIds", () => { expect(cellIds.indexOf("1.0")).not.toBe(-1); expect(cellIds.indexOf("1.1")).not.toBe(-1); }); -}); -describe("S2GeometryProvider.bboxToCellIds", () => { - it("should throw if north east is not larger than south west", () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); - - expect(() => { geometry.bboxToCellIds({ lat: 0, lon: 0 }, { lat: -1, lon: 1 }); }) - .toThrowError(MapillaryError); - expect(() => { geometry.bboxToCellIds({ lat: 0, lon: 0 }, { lat: 1, lon: -1 }); }) - .toThrowError(MapillaryError); - expect(() => { geometry.bboxToCellIds({ lat: 0, lon: 0 }, { lat: -1, lon: -1 }); }) - .toThrowError(MapillaryError); + describe("S2GeometryProvider.bboxToCellIds", () => { + it("should throw if north east is not larger than south west", () => { + const geometry = new S2GeometryProvider(); + + expect(() => { + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: -1, lon: 1 }); + }).toThrowError(MapillaryError); + + expect(() => { + geometry + .bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: 1, lon: -1 }); + }).toThrowError(MapillaryError); + + expect(() => { + geometry.bboxToCellIds( + { lat: 0, lon: 0 }, + { lat: -1, lon: -1 }); + }).toThrowError(MapillaryError); + }); }); - it("should call latLonToCellIds with center and correct threshold", () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); - - spyOn(GeoCoords, "geodeticToEnu").and.returnValue([10, 20, 0]); - const encodeHsSpy: jasmine.Spy = spyOn(geometry, "latLonToCellIds").and.stub(); - - geometry.bboxToCellIds({ lat: 0, lon: 0 }, { lat: 1, lon: 3 }); - - expect(encodeHsSpy.calls.count()).toBe(1); - expect(encodeHsSpy.calls.argsFor(0)[0].lat).toBe(0.5); - expect(encodeHsSpy.calls.argsFor(0)[0].lon).toBe(1.5); + describe("S2GeometryProvider.getVertices", () => { + it("should be correctly placed relative to each other", () => { + const geometry = new S2GeometryProvider(); + + const latLons: LatLon[] = [ + { lat: 0, lon: 0 }, + { lat: 45, lon: 0 }, + { lat: 0, lon: 45 }, + { lat: -45, lon: 0 }, + { lat: 0, lon: -45 }, + { lat: 45, lon: 45 }, + { lat: -45, lon: -45 }, + { lat: 45, lon: -45 }, + { lat: -45, lon: 45 }, + { lat: -45, lon: 135 }, + ]; + + for (let latLon of latLons) { + const cellId = geometry.latLonToCellId(latLon); + const vertices = geometry.getVertices(cellId); + expect(vertices.length).toBe(4); + + const polygon = vertices + .map( + (ll: LatLon): number[] => { + return [ll.lon, ll.lat]; + }); + + expect(isClockwise(polygon)).toBe(true); + } + }); }); -}); -describe("S2GeometryProvider.getCorners", () => { - it("should be correctly placed relative to each other", () => { - const geometry: S2GeometryProvider = new S2GeometryProvider(); - - const latLons: LatLon[] = [ - { lat: 0, lon: 0 }, - { lat: 45, lon: 0 }, - { lat: 0, lon: 45 }, - { lat: -45, lon: 0 }, - { lat: 0, lon: -45 }, - { lat: 45, lon: 45 }, - { lat: -45, lon: -45 }, - { lat: 45, lon: -45 }, - { lat: -45, lon: 45 }, - { lat: -45, lon: 135 }, - ]; - - for (let latLon of latLons) { - const cellId = geometry.latLonToCellId(latLon); - const vertices = geometry.getVertices(cellId); - expect(vertices.length).toBe(4); - - const polygon = vertices - .map( - (ll: LatLon): number[] => { - return [ll.lon, ll.lat]; - }); - - expect(isClockwise(polygon)).toBe(true); - } + describe("S2GeometryProvider.getAdjacent", () => { + it("should always be 8", () => { + const geometry = new S2GeometryProvider(); + + const latLons: LatLon[] = [ + { lat: 0, lon: 0 }, + { lat: 45, lon: 0 }, + { lat: 0, lon: 45 }, + { lat: -45, lon: 0 }, + { lat: 0, lon: -45 }, + { lat: 45, lon: 45 }, + { lat: -45, lon: -45 }, + { lat: 45, lon: -45 }, + { lat: -45, lon: 45 }, + { lat: -45, lon: 135 }, + { lat: -45, lon: 180 }, + { lat: 0, lon: 180 }, + { lat: 45, lon: 180 }, + ]; + + for (let latLon of latLons) { + const cellId = geometry.latLonToCellId(latLon); + const adjacent = geometry.getAdjacent(cellId); + console.log(adjacent); + expect(adjacent.length).toBe(8); + } + }); }); }); diff --git a/spec/graph/Graph.spec.ts b/spec/graph/Graph.spec.ts index a7fbbd767..36d73f6eb 100644 --- a/spec/graph/Graph.spec.ts +++ b/spec/graph/Graph.spec.ts @@ -121,7 +121,6 @@ describe("Graph.cacheBoundingBox$", () => { const h = "h"; spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); const coreImages = @@ -197,7 +196,6 @@ describe("Graph.cacheBoundingBox$", () => { const h = "h"; spyOn(dataProvider.geometry, "bboxToCellIds").and.returnValue([h]); - spyOn(dataProvider.geometry, "latLonToCellIds").and.returnValue([h]); spyOn(dataProvider.geometry, "latLonToCellId").and.returnValue(h); const coreImages = @@ -406,7 +404,7 @@ describe("Graph.cacheFull$", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const coreImages = new Subject(); @@ -478,7 +476,7 @@ describe("Graph.cacheFill$", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const coreImages = new Subject(); @@ -533,7 +531,7 @@ describe("Graph.cacheFill$", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const coreImages = new Subject(); @@ -596,7 +594,7 @@ describe("Graph.cacheFill$", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const coreImages = new Subject(); @@ -652,7 +650,7 @@ describe("Graph.cacheFill$", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const coreImages = new Subject(); @@ -722,7 +720,7 @@ describe("Graph.cacheTiles$", () => { const node = helper.createNode(); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue(["h"]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue(["h"]); const coreImages = new Subject(); @@ -754,7 +752,7 @@ describe("Graph.cacheTiles$", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const imageByKeyResult: ImagesContract = [{ node: fullNode, @@ -798,7 +796,7 @@ describe("Graph.cacheTiles$", () => { const node = helper.createNode(); const h = "h"; - const encodeHsSpy = spyOn(geometryProvider, "latLonToCellIds"); + const encodeHsSpy = spyOn(geometryProvider, "bboxToCellIds"); encodeHsSpy.and.returnValue([h]); const coreImages = @@ -828,7 +826,7 @@ describe("Graph.cacheTiles$", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - const encodeHsSpy = spyOn(geometryProvider, "latLonToCellIds"); + const encodeHsSpy = spyOn(geometryProvider, "bboxToCellIds"); encodeHsSpy.and.returnValue([h]); const imageByKeyResult: ImagesContract = [{ @@ -1199,7 +1197,7 @@ describe("Graph.cacheSequenceNodes$", () => { const graphCalculator = new GraphCalculator(); const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const edgeCalculator = new EdgeCalculator(); const configuration: GraphConfiguration = { @@ -1281,7 +1279,7 @@ describe("Graph.cacheSequenceNodes$", () => { const graphCalculator = new GraphCalculator(); const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const edgeCalculator = new EdgeCalculator(); const configuration: GraphConfiguration = { @@ -1825,7 +1823,7 @@ describe("Graph.cacheSpatialArea$", () => { const h = "h"; spyOn(dataProvider.geometry, "latLonToCellId").and.returnValue(h); - spyOn(dataProvider.geometry, "latLonToCellIds").and.returnValue([h]); + spyOn(dataProvider.geometry, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -1939,7 +1937,7 @@ describe("Graph.cacheSpatialEdges", () => { const dataProvider = new DataProvider(); spyOn(dataProvider.geometry, "latLonToCellId") .and.returnValue(cellId); - spyOn(dataProvider.geometry, "latLonToCellIds") + spyOn(dataProvider.geometry, "bboxToCellIds") .and.returnValue([cellId]); const api = new APIWrapper(dataProvider); const graphCalculator = new GraphCalculator(); @@ -2497,7 +2495,7 @@ describe("Graph.resetSpatialEdges", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - const encodeHsSpy = spyOn(geometryProvider, "latLonToCellIds"); + const encodeHsSpy = spyOn(geometryProvider, "bboxToCellIds"); encodeHsSpy.and.returnValue([h]); const fullNode = helper.createFullNode(); @@ -3055,7 +3053,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3121,7 +3119,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3189,7 +3187,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3256,7 +3254,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3318,7 +3316,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImagesSpy = spyOn(api, "getImages$"); @@ -3604,7 +3602,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3669,7 +3667,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3736,7 +3734,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3802,7 +3800,7 @@ describe("Graph.uncache", () => { const h = "h"; spyOn(geometryProvider, "latLonToCellId").and.returnValue(h); - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([h]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([h]); const getImages = new Subject(); spyOn(api, "getImages$").and.returnValue(getImages); @@ -3923,7 +3921,7 @@ describe("Graph.cacheCell$", () => { const calculator = new GraphCalculator(); const cellId = "cell-id"; - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([cellId]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([cellId]); spyOn(geometryProvider, "latLonToCellId").and.returnValue(cellId); const coreImages = @@ -3996,7 +3994,7 @@ describe("Graph.cacheCell$", () => { const calculator = new GraphCalculator(); const cellId = "cell-id"; - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([cellId]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([cellId]); spyOn(geometryProvider, "latLonToCellId").and.returnValue(cellId); const coreImages = @@ -4089,7 +4087,7 @@ describe("Graph.cacheCell$", () => { const calculator = new GraphCalculator(); const cellId = "cell-id"; - spyOn(geometryProvider, "latLonToCellIds").and.returnValue([cellId]); + spyOn(geometryProvider, "bboxToCellIds").and.returnValue([cellId]); spyOn(geometryProvider, "latLonToCellId").and.returnValue(cellId); const coreImages = diff --git a/src/api/GeohashGeometryProvider.ts b/src/api/GeohashGeometryProvider.ts index 0e4f85e85..9913b8c97 100644 --- a/src/api/GeohashGeometryProvider.ts +++ b/src/api/GeohashGeometryProvider.ts @@ -23,9 +23,7 @@ export class GeohashGeometryProvider extends GeometryProviderBase { /** * Create a new geohash geometry provider instance. */ - constructor(private readonly _level: number = 7) { - super(); - } + constructor(private readonly _level: number = 7) { super(); } /** * Encode the minimum set of geohash tiles containing a bounding box. @@ -42,7 +40,7 @@ export class GeohashGeometryProvider extends GeometryProviderBase { * @returns {string} The geohash tiles containing the bounding box. */ public bboxToCellIds(sw: LatLon, ne: LatLon): string[] { - return this._bboxSquareToCellIds(sw, ne); + return this._approxBboxToCellIds(sw, ne); } /** @inheritdoc */ @@ -84,32 +82,4 @@ export class GeohashGeometryProvider extends GeometryProviderBase { latLon.lon, this._level); } - - /** - * Encode the geohash tiles within a threshold from a position - * using Manhattan distance. - * - * @param {LatLon} latlon - Latitude and longitude to encode. - * @param {number} precision - Precision of the encoding. - * @param {number} threshold - Threshold of the encoding in meters. - * - * @returns {Array} The geohash tiles reachable within the - * threshold. - */ - public latLonToCellIds( - latLon: LatLon, - threshold: number) - : string[] { - const cellId = geohash.encode( - latLon.lat, latLon.lon, this._level); - - const corners = - this._getLatLonBoundingBoxCorners(latLon, threshold); - for (let c of corners) { - if (geohash.encode(c.lat, c.lon, this._level) !== cellId) { - return [cellId, ...this.getAdjacent(cellId)]; - } - } - return [cellId]; - } } diff --git a/src/api/GeometryProviderBase.ts b/src/api/GeometryProviderBase.ts index 3a9366f94..cdf460b07 100644 --- a/src/api/GeometryProviderBase.ts +++ b/src/api/GeometryProviderBase.ts @@ -1,10 +1,9 @@ -import { LatLon } from "./interfaces/LatLon"; - import { MapillaryError } from "../error/MapillaryError"; import { enuToGeodetic, geodeticToEnu, } from "../geo/GeoCoords"; +import { LatLon } from "./interfaces/LatLon"; /** * @class GeometryProviderBase @@ -45,8 +44,7 @@ export abstract class GeometryProviderBase { /** * Get the adjacent cells * - * @param {LatLon} sw - South west corner of the bounding box. - * @param {LatLon} ne - North east corner of the bounding box. + * @param {string} cellId - Id of cell. * @returns {Array} Array of cell ids. No specific * order is guaranteed. */ @@ -79,27 +77,8 @@ export abstract class GeometryProviderBase { throw new MapillaryError("Not implemented"); } - /** - * Convert a geodetic square to cell ids. - * - * The square is specified as a latitude, longitude - * and a threshold from the position using Manhattan distance. - * - * @param {LatLon} latlon - Latitude and longitude. - * @param {number} threshold - Threshold of the conversion in meters. - * - * @returns {Array} Array of cell ids reachable within - * the threshold. - */ - public latLonToCellIds( - latLon: LatLon, - threshold: number) - : string[] { - throw new MapillaryError("Not implemented"); - } - /** @ignore */ - protected _bboxSquareToCellIds(sw: LatLon, ne: LatLon): string[] { + protected _approxBboxToCellIds(sw: LatLon, ne: LatLon): string[] { if (ne.lat <= sw.lat || ne.lon <= sw.lon) { throw new MapillaryError( "North east needs to be top right of south west"); @@ -119,13 +98,13 @@ export abstract class GeometryProviderBase { const threshold = Math.max(enu[0], enu[1]); - return this.latLonToCellIds( + return this._latLonToCellIds( { lat: centerLat, lon: centerLon }, threshold); } /** @ignore */ - protected _enuToGeodetic(point: number[], reference: LatLon): LatLon { + private _enuToGeodetic(point: number[], reference: LatLon): LatLon { const [lat, lon] = enuToGeodetic( point[0], point[1], @@ -138,7 +117,7 @@ export abstract class GeometryProviderBase { } /** @ignore */ - protected _getLatLonBoundingBoxCorners( + private _getLatLonBoundingBoxCorners( latLon: LatLon, threshold: number) : LatLon[] { @@ -153,4 +132,35 @@ export abstract class GeometryProviderBase { return this._enuToGeodetic(point, latLon); }); } + + + /** + * Convert a geodetic square to cell ids. + * + * The square is specified as a latitude, longitude + * and a threshold from the position using Manhattan distance. + * + * @param {LatLon} latlon - Latitude and longitude. + * @param {number} threshold - Threshold of the conversion in meters. + * + * @returns {Array} Array of cell ids reachable within + * the threshold. + * + * @ignore + */ + private _latLonToCellIds( + latLon: LatLon, + threshold: number) + : string[] { + const cellId = this.latLonToCellId(latLon); + const bboxCorners = + this._getLatLonBoundingBoxCorners(latLon, threshold); + for (const corner of bboxCorners) { + const cid = this.latLonToCellId(corner); + if (cid !== cellId) { + return [cellId, ...this.getAdjacent(cellId)]; + } + } + return [cellId]; + } } diff --git a/src/api/S2GeometryProvider.ts b/src/api/S2GeometryProvider.ts index 1aaf01547..9227224a3 100644 --- a/src/api/S2GeometryProvider.ts +++ b/src/api/S2GeometryProvider.ts @@ -22,13 +22,11 @@ export class S2GeometryProvider extends GeometryProviderBase { /** * Create a new S2 geometry provider instance. */ - constructor(private readonly _level: number = 17) { - super(); - } + constructor(private readonly _level: number = 17) { super(); } /** @inheritdoc */ public bboxToCellIds(sw: LatLon, ne: LatLon): string[] { - return this._bboxSquareToCellIds(sw, ne); + return this._approxBboxToCellIds(sw, ne); } /** @inheritdoc */ @@ -79,20 +77,6 @@ export class S2GeometryProvider extends GeometryProviderBase { return this._latLonToId(latLon, this._level); } - /** @inheritdoc */ - public latLonToCellIds(latLon: LatLon, threshold: number): string[] { - const cellId = this._latLonToId(latLon, this._level); - const corners = - this._getLatLonBoundingBoxCorners(latLon, threshold); - - for (const corner of corners) { - if (this._latLonToId(corner, this._level) !== cellId) { - return [cellId, ...this.getAdjacent(cellId)]; - } - } - return [cellId]; - } - private _getNeighbors(s2key: string, level: number): string[] { const latlng = S2.keyToLatLng(s2key); const neighbors = S2.latLngToNeighborKeys( diff --git a/src/graph/Graph.ts b/src/graph/Graph.ts index 49259de07..0a2a5c4bd 100644 --- a/src/graph/Graph.ts +++ b/src/graph/Graph.ts @@ -1240,11 +1240,14 @@ export class Graph { let nodeTiles: NodeTiles = { cache: [], caching: [] }; if (!(key in this._requiredNodeTiles)) { - let node: Node = this.getNode(key); - nodeTiles.cache = this._api.data.geometry - .latLonToCellIds( + const node = this.getNode(key); + const [sw, ne] = this._graphCalculator + .boundingBoxCorners( node.latLon, this._tileThreshold) + + nodeTiles.cache = this._api.data.geometry + .bboxToCellIds(sw, ne) .filter( (h: string): boolean => { return !(h in this._cachedTiles); @@ -1385,59 +1388,60 @@ export class Graph { * reference to either tiles or nodes and may be uncached * even if they are related to the nodes that should be kept. * - * @param {Array} keepKeys - Keys of nodes to keep in + * @param {Array} keepIds - Ids of nodes to keep in * graph unrelated to last access. Tiles related to those keys * will also be kept in graph. - * @param {string} keepSequenceKey - Optional key of sequence + * @param {string} keepSequenceId - Optional id of sequence * for which the belonging nodes should not be disposed or * removed from the graph. These nodes may still be uncached if * not specified in keep keys param. */ - public uncache(keepKeys: string[], keepSequenceKey?: string): void { - let keysInUse: { [key: string]: boolean } = {}; - - this._addNewKeys(keysInUse, this._cachingFull$); - this._addNewKeys(keysInUse, this._cachingFill$); - this._addNewKeys(keysInUse, this._cachingSpatialArea$); - this._addNewKeys(keysInUse, this._requiredNodeTiles); - this._addNewKeys(keysInUse, this._requiredSpatialArea); - - for (let key of keepKeys) { - if (key in keysInUse) { - continue; - } - - keysInUse[key] = true; - } - - let keepHs: { [h: string]: boolean } = {}; - for (let key in keysInUse) { - if (!keysInUse.hasOwnProperty(key)) { - continue; - } - - let node: Node = this._nodes[key]; - - let nodeHs: string[] = this._api.data.geometry - .latLonToCellIds(node.latLon, this._tileThreshold); + public uncache(keepIds: string[], keepSequenceId?: string): void { + let idsInUse: { [id: string]: boolean } = {}; + + this._addNewKeys(idsInUse, this._cachingFull$); + this._addNewKeys(idsInUse, this._cachingFill$); + this._addNewKeys(idsInUse, this._cachingSpatialArea$); + this._addNewKeys(idsInUse, this._requiredNodeTiles); + this._addNewKeys(idsInUse, this._requiredSpatialArea); + + for (const key of keepIds) { + if (key in idsInUse) { continue; } + idsInUse[key] = true; + } + + const tileThreshold = this._tileThreshold; + const calculator = this._graphCalculator; + const geometry = this._api.data.geometry; + const keepCells: { [h: string]: boolean } = {}; + for (let id in idsInUse) { + if (!idsInUse.hasOwnProperty(id)) { continue; } + + const node = this._nodes[id]; + const [sw, ne] = calculator + .boundingBoxCorners( + node.latLon, + tileThreshold, + ) + const nodeCells = geometry.bboxToCellIds(sw, ne) - for (let nodeH of nodeHs) { - if (!(nodeH in keepHs)) { - keepHs[nodeH] = true; + for (const nodeCell of nodeCells) { + if (!(nodeCell in keepCells)) { + keepCells[nodeCell] = true; } } } - let potentialHs: [string, TileAccess][] = []; - for (let h in this._cachedTiles) { - if (!this._cachedTiles.hasOwnProperty(h) || h in keepHs) { + const potentialCells: [string, TileAccess][] = []; + for (let cellId in this._cachedTiles) { + if (!this._cachedTiles.hasOwnProperty(cellId) || + cellId in keepCells) { continue; } - - potentialHs.push([h, this._cachedTiles[h]]); + potentialCells.push([cellId, this._cachedTiles[cellId]]); } - let uncacheHs: string[] = potentialHs + const uncacheCells = potentialCells .sort( (h1: [string, TileAccess], h2: [string, TileAccess]): number => { return h2[1].accessed - h1[1].accessed; @@ -1448,39 +1452,39 @@ export class Graph { return h[0]; }); - for (let uncacheH of uncacheHs) { - this._uncacheTile(uncacheH, keepSequenceKey); + for (let uncacheCell of uncacheCells) { + this._uncacheTile(uncacheCell, keepSequenceId); } - let potentialPreStored: [NodeAccess, string][] = []; - let nonCachedPreStored: [string, string][] = []; - for (let h in this._preStored) { - if (!this._preStored.hasOwnProperty(h) || h in this._cachingTiles$) { + const potentialPreStored: [NodeAccess, string][] = []; + const nonCachedPreStored: [string, string][] = []; + for (let cellId in this._preStored) { + if (!this._preStored.hasOwnProperty(cellId) || + cellId in this._cachingTiles$) { continue; } - const prestoredNodes: { [key: string]: Node } = this._preStored[h]; - - for (let key in prestoredNodes) { - if (!prestoredNodes.hasOwnProperty(key) || key in keysInUse) { + const prestoredNodes = this._preStored[cellId]; + for (let id in prestoredNodes) { + if (!prestoredNodes.hasOwnProperty(id) || id in idsInUse) { continue; } - if (prestoredNodes[key].sequenceId === keepSequenceKey) { + if (prestoredNodes[id].sequenceId === keepSequenceId) { continue; } - if (key in this._cachedNodes) { - potentialPreStored.push([this._cachedNodes[key], h]); + if (id in this._cachedNodes) { + potentialPreStored.push([this._cachedNodes[id], cellId]); } else { - nonCachedPreStored.push([key, h]); + nonCachedPreStored.push([id, cellId]); } } } - let uncachePreStored: [string, string][] = potentialPreStored + const uncachePreStored = potentialPreStored .sort( - ([na1, h1]: [NodeAccess, string], [na2, h2]: [NodeAccess, string]): number => { + ([na1]: [NodeAccess, string], [na2]: [NodeAccess, string]): number => { return na2.accessed - na1.accessed; }) .slice(this._configuration.maxUnusedPreStoredNodes) @@ -1492,61 +1496,61 @@ export class Graph { this._uncachePreStored(nonCachedPreStored); this._uncachePreStored(uncachePreStored); - let potentialNodes: NodeAccess[] = []; - for (let key in this._cachedNodes) { - if (!this._cachedNodes.hasOwnProperty(key) || key in keysInUse) { + const potentialNodes: NodeAccess[] = []; + for (let id in this._cachedNodes) { + if (!this._cachedNodes.hasOwnProperty(id) || id in idsInUse) { continue; } - potentialNodes.push(this._cachedNodes[key]); + potentialNodes.push(this._cachedNodes[id]); } - let uncacheNodes: NodeAccess[] = potentialNodes + const uncacheNodes = potentialNodes .sort( (n1: NodeAccess, n2: NodeAccess): number => { return n2.accessed - n1.accessed; }) .slice(this._configuration.maxUnusedNodes); - for (let nodeAccess of uncacheNodes) { + for (const nodeAccess of uncacheNodes) { nodeAccess.node.uncache(); - let key: string = nodeAccess.node.id; - delete this._cachedNodes[key]; + const id = nodeAccess.node.id; + delete this._cachedNodes[id]; - if (key in this._cachedNodeTiles) { - delete this._cachedNodeTiles[key]; + if (id in this._cachedNodeTiles) { + delete this._cachedNodeTiles[id]; } - if (key in this._cachedSpatialEdges) { - delete this._cachedSpatialEdges[key]; + if (id in this._cachedSpatialEdges) { + delete this._cachedSpatialEdges[id]; } } - let potentialSequences: SequenceAccess[] = []; - for (let sequenceKey in this._sequences) { - if (!this._sequences.hasOwnProperty(sequenceKey) || - sequenceKey in this._cachingSequences$ || - sequenceKey === keepSequenceKey) { + const potentialSequences: SequenceAccess[] = []; + for (let sequenceId in this._sequences) { + if (!this._sequences.hasOwnProperty(sequenceId) || + sequenceId in this._cachingSequences$ || + sequenceId === keepSequenceId) { continue; } - potentialSequences.push(this._sequences[sequenceKey]); + potentialSequences.push(this._sequences[sequenceId]); } - let uncacheSequences: SequenceAccess[] = potentialSequences + const uncacheSequences = potentialSequences .sort( (s1: SequenceAccess, s2: SequenceAccess): number => { return s2.accessed - s1.accessed; }) .slice(this._configuration.maxSequences); - for (let sequenceAccess of uncacheSequences) { - let sequenceKey: string = sequenceAccess.sequence.id; + for (const sequenceAccess of uncacheSequences) { + const sequenceId = sequenceAccess.sequence.id; - delete this._sequences[sequenceKey]; + delete this._sequences[sequenceId]; - if (sequenceKey in this._cachedSequenceNodes) { - delete this._cachedSequenceNodes[sequenceKey]; + if (sequenceId in this._cachedSequenceNodes) { + delete this._cachedSequenceNodes[sequenceId]; } sequenceAccess.sequence.dispose();