Skip to content

Commit

Permalink
Resolves #1636 - Offline routing
Browse files Browse the repository at this point in the history
  • Loading branch information
HarelM committed Mar 29, 2023
1 parent f3d357a commit a0a6ead
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,20 @@ import { HttpClientModule } from "@angular/common/http";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { MockNgRedux, MockNgReduxModule } from "@angular-redux2/store/testing";

import { ResourcesService } from "./resources.service";
import { ElevationProvider } from "./elevation.provider";
import { ToastService } from "./toast.service";
import { ToastServiceMockCreator } from "./toast.service.spec";
import { LoggingService } from "./logging.service";
import { DatabaseService } from "./database.service";

describe("ElevationProvider", () => {

beforeEach(() => {
let toastMockCreator = new ToastServiceMockCreator();
TestBed.configureTestingModule({
imports: [
HttpClientModule,
HttpClientTestingModule,
MockNgReduxModule
],
providers: [
{ provide: ResourcesService, useValue: toastMockCreator.resourcesService },
{ provide: ToastService, useValue: toastMockCreator.toastService },
{ provide: LoggingService, useValue: { warning: () => { } } },
{ provide: DatabaseService, useValue: {} },
ElevationProvider
Expand All @@ -42,7 +36,8 @@ describe("ElevationProvider", () => {

mockBackend.match(() => true)[0].flush([1]);
return promise;
}));
}
));

it("Should not call provider bacause all coordinates has elevation", inject([ElevationProvider],
async (elevationProvider: ElevationProvider) => {
Expand All @@ -52,20 +47,56 @@ describe("ElevationProvider", () => {
elevationProvider.updateHeights(latlngs).then(() => {
expect(latlngs[0].alt).toBe(1);
});
}));
}
));

it("Should raise toast when error occurs", inject([ElevationProvider, HttpTestingController, ToastService],
async (elevationProvider: ElevationProvider, mockBackend: HttpTestingController, toastService: ToastService) => {
it("Should not update elevation when getting an error from server and offline is not available",
inject([ElevationProvider, HttpTestingController],
async (elevationProvider: ElevationProvider, mockBackend: HttpTestingController) => {

let latlngs = [{ lat: 0, lng: 0, alt: 0 }];

let promise = elevationProvider.updateHeights(latlngs);
promise.then(() => {
expect(latlngs[0].alt).toBe(0);
}, () => fail());

mockBackend.match(() => true)[0].flush(null, { status: 500, statusText: "Server Error" });
return promise;
}
));

it("Should update elevation when getting an error from server and offline is available",
inject([ElevationProvider, HttpTestingController, DatabaseService],
async (elevationProvider: ElevationProvider, mockBackend: HttpTestingController, db: DatabaseService) => {
let latlngs = [{ lat: 0, lng: 0, alt: 0 }];
spyOn(toastService, "warning");

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

// create a blue image 256x256
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 256;
canvas.height = 256;
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
let url = canvas.toDataURL("image/png");

db.getTile = () => fetch(url).then(r => r.arrayBuffer());

let promise = elevationProvider.updateHeights(latlngs);
promise.then(() => {
expect(toastService.warning).toHaveBeenCalled();
expect(latlngs[0].alt).not.toBe(0);
}, () => fail());

mockBackend.match(() => true)[0].flush(null, { status: 500, statusText: "Server Error" });
return promise;
}));
}
));
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { NgRedux } from "@angular-redux2/store";
import { timeout } from "rxjs/operators";
import { firstValueFrom } from "rxjs";

import { ResourcesService } from "./resources.service";
import { ToastService } from "./toast.service";
import { LoggingService } from "./logging.service";
import { SpatialService } from "./spatial.service";
import { DatabaseService } from "./database.service";
Expand All @@ -21,8 +19,6 @@ export class ElevationProvider {
private elevationCache: Map<string, Uint8ClampedArray>;

constructor(private readonly httpClient: HttpClient,
private readonly resources: ResourcesService,
private readonly toastService: ToastService,
private readonly loggingService: LoggingService,
private readonly databaseService: DatabaseService,
private readonly ngRedux: NgRedux<ApplicationState>) {
Expand Down Expand Up @@ -59,7 +55,6 @@ export class ElevationProvider {
} catch (ex2) {
this.loggingService.warning(`[Elevation] Unable to get elevation data for ${latlngs.length} points. ` +
`${(ex as Error).message}, ${(ex2 as Error).message}`);
this.toastService.warning(this.resources.unableToGetElevationData);
}
}
}
Expand All @@ -75,9 +70,6 @@ export class ElevationProvider {
const tileXmin = Math.min(...tiles.map(tile => Math.floor(tile.x)));
const tileYmax = Math.max(...tiles.map(tile => Math.floor(tile.y)));
const tileYmin = Math.min(...tiles.map(tile => Math.floor(tile.y)));
if (tileXmax - tileXmin > 1 || tileYmax - tileYmin > 1) {
throw new Error("[Elevation] Getting elevation is only supported for adjecent tiles maximum...");
}
for (let tileX = tileXmin; tileX <= tileXmax; tileX++) {
for (let tileY = tileYmin; tileY <= tileYmax; tileY++) {
let key = `${tileX}/${tileY}`;
Expand Down
10 changes: 5 additions & 5 deletions IsraelHiking.Web/src/application/services/resources.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ export class ResourcesService {
public unableToLoadFromUrl: string;
public routeNameAlreadyInUse: string;
public unableToGenerateUrl: string;
public unableToGetElevationData: string;
public routingFailed: string;
public routingFailedTryShorterRoute: string;
public routingFailedBuySubscription: string;
public unableToLogin: string;
public unableToSendRoute: string;
public noUnmappedRoutes: string;
Expand Down Expand Up @@ -478,7 +478,7 @@ export class ResourcesService {
}

private async setLanguageInternal(language: Language): Promise<void> {
await this.gettextCatalog.loadRemote(Urls.translations + language.code + ".json?sign=1679061617453");
await this.gettextCatalog.loadRemote(Urls.translations + language.code + ".json?sign=1680089205399");
this.about = this.gettextCatalog.getString("About");
this.legend = this.gettextCatalog.getString("Legend");
this.clear = this.gettextCatalog.getString("Clear");
Expand Down Expand Up @@ -717,8 +717,8 @@ export class ResourcesService {
this.unableToLoadFromUrl = this.gettextCatalog.getString("Unable to load from URL...");
this.routeNameAlreadyInUse = this.gettextCatalog.getString("The route's name was altered since it is in use...");
this.unableToGenerateUrl = this.gettextCatalog.getString("Unable to generate URL, please try again later...");
this.unableToGetElevationData = this.gettextCatalog.getString("Unable to get elevation data:");
this.routingFailed = this.gettextCatalog.getString("Routing failed:");
this.routingFailedTryShorterRoute = this.gettextCatalog.getString("Routing failed, please try a shorter route...");
this.routingFailedBuySubscription = this.gettextCatalog.getString("Routing failed, consider buying a subscription.");
this.unableToLogin = this.gettextCatalog.getString("Unable to login...");
this.unableToSendRoute = this.gettextCatalog.getString("Unable to send route...");
this.noUnmappedRoutes = this.gettextCatalog.getString("No unmapped routes! :-)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import { RunningContextService } from "./running-context.service";
import geojsonVt from "geojson-vt";
import vtpbf from "vt-pbf";

function createTileFromFeatureCollection(featureCollection: GeoJSON.FeatureCollection): ArrayBuffer {
const 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 All @@ -34,8 +34,8 @@ describe("Router Service", () => {
{ provide: ResourcesService, useValue: toastMockCreator.resourcesService },
{ provide: ToastService, useValue: toastMockCreator.toastService },
{ provide: DatabaseService, usevalue: {} },
{ provide: LoggingService, useValue: {} },
{ provide: RunningContextService, useValue: null },
{ provide: LoggingService, useValue: { error: () => {} } },
{ provide: RunningContextService, useValue: {} },
GeoJsonParser,
RouterService
]
Expand Down
28 changes: 20 additions & 8 deletions IsraelHiking.Web/src/application/services/router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { ResourcesService } from "./resources.service";
import { ToastService } from "./toast.service";
import { SpatialService } from "./spatial.service";
import { DatabaseService } from "./database.service";
import { LoggingService } from "./logging.service";
import { RunningContextService } from "./running-context.service";
import { Urls } from "../urls";
import type { ApplicationState, LatLngAlt, RoutingType } from "../models/models";

Expand All @@ -22,6 +24,8 @@ export class RouterService {
private readonly resources: ResourcesService,
private readonly toastService: ToastService,
private readonly databaseService: DatabaseService,
private readonly loggingService: LoggingService,
private readonly runningContextService: RunningContextService,
private readonly ngRedux: NgRedux<ApplicationState>) {
this.featuresCache = new Map<string, GeoJSON.FeatureCollection<GeoJSON.LineString>>();
}
Expand All @@ -37,10 +41,11 @@ export class RouterService {
try {
return await this.getOffineRoute(latlngStart, latlngEnd, routinType);
} catch (ex2) {
// HM TODO: consider adding message about offline routing
this.toastService.error({
message: (ex as Error).message + ", " + (ex2 as Error).message
}, this.resources.routingFailed);
this.loggingService.error(`[Routing] failed: ${(ex as Error).message}, ${(ex2 as Error).message}`);
this.toastService.warning(this.ngRedux.getState().offlineState.isOfflineAvailable || !this.runningContextService.isCapacitor
? this.resources.routingFailedTryShorterRoute
: this.resources.routingFailedBuySubscription
);
return [latlngStart, latlngEnd];
}
}
Expand All @@ -52,17 +57,24 @@ export class RouterService {
}
let offlineState = this.ngRedux.getState().offlineState;
if (!offlineState.isOfflineAvailable || offlineState.lastModifiedDate == null) {
throw new Error("[Routing] Offline routing is only supported after downloading offline data");
throw new Error("Offline routing is only supported after downloading offline data");
}
const zoom = 14; // this is the max zoom for these tiles
let tiles = [latlngStart, latlngEnd].map(latlng => SpatialService.toTile(latlng, zoom));
const tileXmax = Math.max(...tiles.map(tile => Math.floor(tile.x)));
let tileXmax = Math.max(...tiles.map(tile => Math.floor(tile.x)));
const tileXmin = Math.min(...tiles.map(tile => Math.floor(tile.x)));
const tileYmax = Math.max(...tiles.map(tile => Math.floor(tile.y)));
let tileYmax = Math.max(...tiles.map(tile => Math.floor(tile.y)));
const tileYmin = Math.min(...tiles.map(tile => Math.floor(tile.y)));
if (tileXmax - tileXmin > 2 || tileYmax - tileYmin > 2) {
throw new Error("[Routing] Offline routing is only supported for adjecent tiles maximum...");
throw new Error("Offline routing is only supported for adjecent tiles maximum...");
}
// increase the chance of getting a route by adding more tiles
if (tileXmax === tileXmin) {
tileXmax += 1;
};
if (tileYmax === tileYmin) {
tileYmax += 1;
};
let features = await this.updateCacheAndGetFeatures(tileXmin, tileXmax, tileYmin, tileYmax, zoom);
if (routinType === "4WD") {
features = features.filter(f =>
Expand Down
4 changes: 2 additions & 2 deletions IsraelHiking.Web/src/translations/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,8 @@
"Route Statistics": "Route Statistics",
"Routes": "Routes",
"Routes and Points": "Routes and Points",
"Routing failed:": "Routing failed:",
"Routing failed, consider buying a subscription.": "Routing failed, consider buying a subscription for offline maps.",
"Routing failed, please try a shorter route...": "Routing failed, please try a shorter route...",
"Ruins": "Ruins",
"Running in the background": "Running in the background",
"Runway and Taxiway": "Airfield with Runway and Taxiway",
Expand Down Expand Up @@ -369,7 +370,6 @@
"Unable to find the required point of interest...": "Unable to find the required point of interest...",
"Unable to find your location...": "Unable to find your location...",
"Unable to generate URL, please try again later...": "Unable to generate URL, please try again later...",
"Unable to get elevation data:": "Unable to get elevation data:",
"Unable to get search results...": "Unable to get search results...",
"Unable to load from URL...": "Unable to load from URL...",
"Unable to login...": "Unable to login...",
Expand Down
4 changes: 2 additions & 2 deletions IsraelHiking.Web/src/translations/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,8 @@
"Route Statistics": "סטטיסטיקת מסלול",
"Routes": "מסלולים",
"Routes and Points": "מסלולים ונקודות",
"Routing failed:": "הניווט נכשל:",
"Routing failed, consider buying a subscription.": "הניווט נכשל, אנו ממליצים לרכוש מנוי למפות ללא רשת.",
"Routing failed, please try a shorter route...": "הניווט נכשל, אנא נסו לצייר מסלול קצר יותר...",
"Ruins": "חורבות",
"Running in the background": "רצה ברקע",
"Runway and Taxiway": "שדה תעופה עם מסלולי המראה והסעה",
Expand Down Expand Up @@ -369,7 +370,6 @@
"Unable to find the required point of interest...": "אין אפשרות למצוא את נקודת העניין המבוקשת...",
"Unable to find your location...": "אין אפשרות למצוא את מיקומך...",
"Unable to generate URL, please try again later...": "לא ניתן ליצור קישור, אנא נסו מאוחר יותר...",
"Unable to get elevation data:": "לא ניתן לקבל מידע על גבהים...",
"Unable to get search results...": "לא ניתן לאחזר תוצאות חיפוש...",
"Unable to load from URL...": "לא ניתן לפתוח את הקישור...",
"Unable to login...": "הכניסה נכשלה...",
Expand Down

0 comments on commit a0a6ead

Please sign in to comment.