Skip to content

Commit

Permalink
Offline routing #1636 - Add missing multiline string and reduced tole…
Browse files Browse the repository at this point in the history
…race for tile edges
  • Loading branch information
HarelM committed Mar 27, 2023
1 parent 8ca77ee commit 536cc8e
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 17 deletions.
16 changes: 12 additions & 4 deletions IsraelHiking.Web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion IsraelHiking.Web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsdoc": "39.3.6",
"eslint-plugin-prefer-arrow": "1.2.3",
"geojson-vt": "^3.2.1",
"gettext-extractor": "3.5.4",
"jasmine-core": "~4.4.0",
"jasmine-spec-reporter": "~7.0.0",
Expand All @@ -136,7 +137,8 @@
"po2json": "0.4.5",
"request": "^2.88.2",
"shelljs": "^0.8.5",
"typescript": "4.8.3"
"typescript": "4.8.3",
"vt-pbf": "^3.1.3"
},
"license": "CC-BY-NC-3.0",
"name": "israel-hiking",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ declare module "linear-interpolator";
declare module "piexifjs";
declare module "d3-regression";
declare module "xml-beautify";
declare module "vt-pbf";
declare module "geojson-vt";

declare module "file-saver-es" {
const saveAsFunction: typeof saveAs;
Expand Down
196 changes: 189 additions & 7 deletions IsraelHiking.Web/src/application/services/router.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import { ToastServiceMockCreator } from "./toast.service.spec";
import { DatabaseService } from "./database.service";
import { LoggingService } from "./logging.service";
import { RunningContextService } from "./running-context.service";
import geojsonVt from "geojson-vt";
import vtpbf from "vt-pbf";

// Tile from https://israelhiking.osm.org.il/vector/data/IHM/14/9788/6769.pbf that has a single highway in it
// See here: https://israelhiking.osm.org.il/map/15.13/29.8131/35.0839
// eslint-disable-next-line
const base64Tile = "GoYECg50cmFuc3BvcnRhdGlvbhLOARIKAAAUARYCGgAbABgCIr0BCYBBiAmSBC8UzwFabTC5AVClAUDxAWaPAmw3GN8BXvEBZsMBUpMBOu8BZN8BXpcCfoMBPjseaTZ3PqkBVk8oXTZbNIcBUI0BVlc0TTBlQmtIowFwvQGEAYUBYo8BbJMBds0BqgGhAYgBX1Z1aokBfokBiAGjAaABXWSjAa4BlwGqAWFyR1ZRZnOQAUFSNURrkgF9rgF5rgFfjgFXhgFbkgFJeGOoAVeaAUWCAVGWAVOsAUeSAVvGAUusARtEGgVjbGFzcxoIc3ViY2xhc3MaB25ldHdvcmsaBm9uZXdheRoEcmFtcBoHYnJ1bm5lbBoHc2VydmljZRoGYWNjZXNzGgR0b2xsGgpleHByZXNzd2F5GgVsYXllchoFbGV2ZWwaBmluZG9vchoHYmljeWNsZRoEZm9vdBoFaG9yc2UaCW10Yl9zY2FsZRoJdHJhY2t0eXBlGgZjb2xvdXIaB2NvbG91cjIaB3N1cmZhY2UaCm9uZXdheTptdGIaCG1heHNwZWVkGghjeWNsZXdheRoNY3ljbGV3YXk6bGVmdBoOY3ljbGV3YXk6cmlnaHQaCWlobV9jbGFzcxoLaWxtdGJfY2xhc3MiBwoFdHJ1bmsiBwoFcGF2ZWQiBQoDMTAwKIAgeAIapQQKE3RyYW5zcG9ydGF0aW9uX25hbWUS2AESDgAAAQACAAwBDQIOAw8EGAIiwwEJgELUCKIErwFIzwFabTC5AVClAUDxAWaPAmw3GN8BXvEBZsMBUpMBOu8BZN8BXpcCfoMBPjseaTZ3PqkBVk8oXTZbNIcBUI0BVlc0TTBlQmtIowFwvQGEAYUBYo8BbJMBds0BqgGhAYgBX1Z1aokBfokBiAGjAaABXWSjAa4BlwGqAWFyR1ZRZnOQAUFSNURrkgF9rgF5rgFfjgFXhgFbkgFJeGOoAVeaAUWCAVGWAVOsAUeSAVvGAUusAS1wR6IBJV4aBG5hbWUaB25hbWVfZW4aB25hbWVfZGUaCG10YjpuYW1lGgttdGI6bmFtZTplbhoLbXRiOm5hbWU6aGUaB25hbWU6ZGUaB25hbWU6ZW4aB25hbWU6aGUaCG5hbWVfaW50GgpuYW1lOmxhdGluGg1uYW1lOm5vbmxhdGluGgNyZWYaCnJlZl9sZW5ndGgaB25ldHdvcmsaBWNsYXNzGghzdWJjbGFzcxoHYnJ1bm5lbBoFbGF5ZXIaBWxldmVsGgZpbmRvb3IaB3JvdXRlXzEaB3JvdXRlXzIaB3JvdXRlXzMaB3JvdXRlXzQaB3JvdXRlXzUaB3JvdXRlXzYiFwoVSm9yZGFuIFZhbGxleSBIaWdod2F5IgQKAjY1IgIoAiIGCgRyb2FkIgcKBXRydW5rKIAgeAIa/gEKCGl0bV9ncmlkEhMSCAAAAQECAgMDGAEiBQmCFbwYEhMSCAAEAQECAwMDGAEiBQmSM7gYEhMSCAAAAQUCAgMCGAEiBQmGFeA2EhYSBAAEAgMYAiIMCZgzgEESBccoA7cZEhMSBAAAAgIYAiIJCYgVgEEKCf9BEhISBAEFAwIYAiIICX/iNgqAQgcSGBIEAQEDAxgCIg4Jf74YGoIWAZAeA+4NARITEggABAEFAgMDAhgBIgUJljPcNhoEZWFzdBoFbm9ydGgaCWVhc3RfcmFuaxoKbm9ydGhfcmFuayIDKM8BIgMongMiAigBIgIoAiIDKNABIgMonQMogCB4Ag==";
function createTileFromFeatureCollection(featureCollection: GeoJSON.FeatureCollection): ArrayBuffer {
let tileindex = geojsonVt(featureCollection);
let tile = tileindex.getTile(14, 8192, 8191);
return vtpbf.fromGeojsonVt({ geojsonLayer: tile });

}

describe("Router Service", () => {
beforeEach(() => {
Expand Down Expand Up @@ -139,7 +143,185 @@ describe("Router Service", () => {
inject([RouterService, HttpTestingController, DatabaseService],
async (router: RouterService, mockBackend: HttpTestingController, db: DatabaseService) => {

db.getTile = () => fetch(`data:application/x-protobuf;base64,${base64Tile}`).then(r => r.arrayBuffer());
let featureCollection = {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: {
type: "LineString",
coordinates: [[0.0001,0.0001], [0.0001,0.0002], [0.0001,0.0003]]
},
properties: {
ihm_class: "track"
}
}]
} as GeoJSON.FeatureCollection;

db.getTile = () => Promise.resolve(createTileFromFeatureCollection(featureCollection));

MockNgRedux.store.getState = () => ({
offlineState: {
isOfflineAvailable: true,
lastModifiedDate: new Date()
}
});

let promise = router.getRoute({ lat: 0.0001, lng: 0.0001 }, { lat: 0.0005, lng: 0.0001 }, "Hike").then((data) => {
expect(data.length).toBe(3);
}, fail);

mockBackend.expectOne(() => true).flush(null, { status: 500, statusText: "Server error" });
return promise;
}
));

it("Should return a route when getting error response from server and offline is available for a multiline string",
inject([RouterService, HttpTestingController, DatabaseService],
async (router: RouterService, mockBackend: HttpTestingController, db: DatabaseService) => {

let featureCollection = {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: {
type: "MultiLineString",
coordinates: [
[[0.0001,0.0001], [0.0001,0.0002], [0.0001,0.0003]],
[[0.0001,0.0003], [0.0002,0.0003], [0.0003,0.0003]]
]
},
properties: {
ihm_class: "track"
}
}]
} as GeoJSON.FeatureCollection;

db.getTile = () => Promise.resolve(createTileFromFeatureCollection(featureCollection));

MockNgRedux.store.getState = () => ({
offlineState: {
isOfflineAvailable: true,
lastModifiedDate: new Date()
}
});

let promise = router.getRoute({ lat: 0.0001, lng: 0.0001 }, { lat: 0.0005, lng: 0.0005 }, "Hike").then((data) => {
expect(data.length).toBe(5);
}, fail);

mockBackend.expectOne(() => true).flush(null, { status: 500, statusText: "Server error" });
return promise;
}
));

it("Should return a route when getting error response from server and offline is available only through one line",
inject([RouterService, HttpTestingController, DatabaseService],
async (router: RouterService, mockBackend: HttpTestingController, db: DatabaseService) => {

let featureCollection = {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: {
type: "LineString",
coordinates: [[0.0001,0.0001], [0.0001,0.0002], [0.0001,0.0003]]
},
properties: {
ihm_class: "track"
}
}, {
type: "Feature",
geometry: {
type: "LineString",
coordinates: [[0.0001,0.0003], [0.0002,0.0003], [0.0003,0.0003]]
},
properties: {
ihm_class: "steps"
}
}]
} as GeoJSON.FeatureCollection;

db.getTile = () => Promise.resolve(createTileFromFeatureCollection(featureCollection));

MockNgRedux.store.getState = () => ({
offlineState: {
isOfflineAvailable: true,
lastModifiedDate: new Date()
}
});

let promise = router.getRoute({ lat: 0.0001, lng: 0.0001 }, { lat: 0.0005, lng: 0.0005 }, "Bike").then((data) => {
expect(data.length).toBe(3);
}, fail);

mockBackend.expectOne(() => true).flush(null, { status: 500, statusText: "Server error" });
return promise;
}
));

it("Should return srart and end point when all lines are filtered out",
inject([RouterService, HttpTestingController, DatabaseService],
async (router: RouterService, mockBackend: HttpTestingController, db: DatabaseService) => {

let featureCollection = {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: {
type: "LineString",
coordinates: [[0.0001,0.0003], [0.0002,0.0003], [0.0003,0.0003]]
},
properties: {
ihm_class: "path"
}
}]
} as GeoJSON.FeatureCollection;

db.getTile = () => Promise.resolve(createTileFromFeatureCollection(featureCollection));

MockNgRedux.store.getState = () => ({
offlineState: {
isOfflineAvailable: true,
lastModifiedDate: new Date()
}
});

let promise = router.getRoute({ lat: 0.0001, lng: 0.0001 }, { lat: 0.0005, lng: 0.0005 }, "4WD").then((data) => {
expect(data.length).toBe(2);
}, fail);

mockBackend.expectOne(() => true).flush(null, { status: 500, statusText: "Server error" });
return promise;
}
));

it("Should return a route between two lines when points are not exactly the same",
inject([RouterService, HttpTestingController, DatabaseService],
async (router: RouterService, mockBackend: HttpTestingController, db: DatabaseService) => {
let featureCollection = {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: {
type: "LineString",
coordinates: [[0.0001,0.0001], [0.0001,0.0002], [0.0001,0.0003]]
},
properties: {
ihm_class: "major"
}
}, {
type: "Feature",
geometry: {
type: "LineString",
coordinates: [[0.0001,0.000305], [0.0002,0.0003], [0.0003,0.0003]]
},
properties: {
ihm_class: "minor"
}
}]
} as GeoJSON.FeatureCollection;

db.getTile = () => Promise.resolve(createTileFromFeatureCollection(featureCollection));

MockNgRedux.store.getState = () => ({
offlineState: {
Expand All @@ -148,8 +330,8 @@ describe("Router Service", () => {
}
});

let promise = router.getRoute({ lat: 29.807326, lng: 35.071012 }, { lat: 29.817968, lng: 35.088073 }, "Hike").then((data) => {
expect(data.length).toBe(48);
let promise = router.getRoute({ lat: 0.0001, lng: 0.0001 }, { lat: 0.0005, lng: 0.0005 }, "Bike").then((data) => {
expect(data.length).toBe(5);
}, fail);

mockBackend.expectOne(() => true).flush(null, { status: 500, statusText: "Server error" });
Expand Down
17 changes: 13 additions & 4 deletions IsraelHiking.Web/src/application/services/router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class RouterService {
type: "FeatureCollection",
features
} as GeoJSON.FeatureCollection<GeoJSON.LineString>;
let pathFinder = new PathFinder(collection);
let pathFinder = new PathFinder(collection, {tolerance: 2e-5});
let route = pathFinder.findPath(startFeature, endFeature);
if (!route) {
throw new Error("[Routing] No route found... :-(");
Expand Down Expand Up @@ -122,10 +122,19 @@ export class RouterService {
continue;
}
let geojsonFeature = feature.toGeoJSON(tileX, tileY, zoom);
if (geojsonFeature.geometry.type !== "LineString") {
continue;
if (geojsonFeature.geometry.type === "LineString") {
collection.features.push(geojsonFeature as GeoJSON.Feature<GeoJSON.LineString>);
} else if (geojsonFeature.geometry.type === "MultiLineString") {
let multiLines = geojsonFeature.geometry.coordinates.map(coordinates => ({
type: "Feature",
geometry: {
type: "LineString",
coordinates
},
properties: {...geojsonFeature.properties}
} as GeoJSON.Feature<GeoJSON.LineString>));
collection.features.push(...multiLines);
}
collection.features.push(geojsonFeature as GeoJSON.Feature<GeoJSON.LineString>);
}
}
collection.features = SpatialService.clipLinesToTileBoundary(collection.features, { x: tileX, y: tileY}, zoom);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ export class SpatialService {
}
}
}

}

public static splitLine(newLatlng: LatLngAlt, line: LatLngAlt[]): { start: LatLngAlt[]; end: LatLngAlt[] } {
Expand Down

0 comments on commit 536cc8e

Please sign in to comment.