Skip to content

Commit

Permalink
Add iNature service to client side.
Browse files Browse the repository at this point in the history
  • Loading branch information
HarelM committed Sep 3, 2024
1 parent d2b15ee commit 9cef37a
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 168 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { SpatialService } from "../../services/spatial.service";
import { NavigateHereService } from "../../services/navigate-here.service";
import { SetSelectedPoiAction } from "../../reducers/poi.reducer";
import { AddPrivatePoiAction } from "../../reducers/routes.reducer";
import { GeoJSONUtils } from "../../services/geojson-utils";
import type { ApplicationState, LatLngAlt, LinkData, Overlay } from "../../models/models";

@Component({
Expand Down Expand Up @@ -132,11 +133,11 @@ export class LayersViewComponent extends BaseMapComponent implements OnInit {
}

public getTitle(feature: GeoJSON.Feature<GeoJSON.Point>): string {
return this.poiService.getTitle(feature, this.resources.getCurrentLanguageCodeSimplified());
return GeoJSONUtils.getTitle(feature, this.resources.getCurrentLanguageCodeSimplified());
}

public hasExtraData(feature: GeoJSON.Feature<GeoJSON.Point>): boolean {
return this.poiService.hasExtraData(feature, this.resources.getCurrentLanguageCodeSimplified());
return GeoJSONUtils.hasExtraData(feature, this.resources.getCurrentLanguageCodeSimplified());
}

public isCoordinatesFeature(feature: Immutable<GeoJSON.Feature>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Router } from "@angular/router";

import { BaseMapComponent } from "../base-map.component";
import { ResourcesService } from "../../services/resources.service";
import { PoiService } from "../../services/poi.service";
import { RouteStrings } from "../../services/hash.service";
import { GeoJSONUtils } from "../../services/geojson-utils";

@Component({
selector: "cluster-overlay",
Expand All @@ -20,18 +20,17 @@ export class ClusterOverlayComponent extends BaseMapComponent {
public closed: EventEmitter<void>;

constructor(resources: ResourcesService,
private readonly router: Router,
private readonly poiService: PoiService) {
private readonly router: Router) {
super(resources);
this.closed = new EventEmitter();
}

public getTitle(feature: GeoJSON.Feature) {
return this.poiService.getTitle(feature, this.resources.getCurrentLanguageCodeSimplified());
return GeoJSONUtils.getTitle(feature, this.resources.getCurrentLanguageCodeSimplified());
}

public hasExtraData(feature: GeoJSON.Feature): boolean {
return this.poiService.hasExtraData(feature, this.resources.getCurrentLanguageCodeSimplified());
return GeoJSONUtils.hasExtraData(feature, this.resources.getCurrentLanguageCodeSimplified());
}

public clickOnItem(feature: GeoJSON.Feature) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { GeoJsonParser } from "../../../services/geojson.parser";
import { sidebarAnimate } from "../sidebar.component";
import { AddRouteAction, AddPrivatePoiAction } from "../../../reducers/routes.reducer";
import { SetSelectedPoiAction, SetUploadMarkerDataAction, SetSidebarAction } from "../../../reducers/poi.reducer";
import { GeoJSONUtils } from "../../../services/geojson-utils";
import type {
LinkData,
LatLngAlt,
Expand Down Expand Up @@ -181,13 +182,13 @@ export class PublicPoiSidebarComponent extends BaseMapComponent implements OnDes

private initFromFeature(feature: GeoJSON.Feature) {
this.fullFeature = feature;
this.latlng = this.poiService.getLocation(feature);
this.latlng = GeoJSONUtils.getLocation(feature);
this.sourceImageUrls = this.getSourceImageUrls(feature);
this.shareLinks = this.poiService.getPoiSocialLinks(feature);
this.contribution = this.poiService.getContribution(feature);
this.info = this.poiService.getEditableDataFromFeature(feature);
const language = this.resources.getCurrentLanguageCodeSimplified();
this.titleService.set(this.poiService.getTitle(feature, language));
this.titleService.set(GeoJSONUtils.getTitle(feature, language));
}

private getSourceImageUrls(feature: GeoJSON.Feature): SourceImageUrlPair[] {
Expand Down Expand Up @@ -228,8 +229,8 @@ export class PublicPoiSidebarComponent extends BaseMapComponent implements OnDes
return "";
}
const language = this.resources.getCurrentLanguageCodeSimplified();
const description = this.poiService.getDescription(this.fullFeature, language) ||
this.poiService.getExternalDescription(this.fullFeature, language);
const description = GeoJSONUtils.getDescription(this.fullFeature, language) ||
GeoJSONUtils.getExternalDescription(this.fullFeature, language);
if (description) {
return description;
}
Expand Down Expand Up @@ -326,8 +327,8 @@ export class PublicPoiSidebarComponent extends BaseMapComponent implements OnDes
}

public navigateHere() {
const location = this.poiService.getLocation(this.fullFeature);
const title = this.poiService.getTitle(this.fullFeature, this.resources.getCurrentLanguageCodeSimplified());
const location = GeoJSONUtils.getLocation(this.fullFeature);
const title = GeoJSONUtils.getTitle(this.fullFeature, this.resources.getCurrentLanguageCodeSimplified());
this.navigateHereService.addNavigationSegment(location, title);
}

Expand Down
42 changes: 42 additions & 0 deletions IsraelHiking.Web/src/application/services/geojson-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { GeoJSONUtils } from "./geojson-utils";

describe("GeoJsonUtils", () => {
it("should get extenal description for hebrew", () => {
const results = GeoJSONUtils.getExternalDescription(
{properties: { "poiExternalDescription:he": "desc"}} as any as GeoJSON.Feature, "he");
expect(results).toBe("desc");
});

it("should get extenal description for language independant", () => {
const results = GeoJSONUtils.getExternalDescription(
{properties: { poiExternalDescription: "desc"}} as any as GeoJSON.Feature, "he");
expect(results).toBe("desc");
});

it("should get title when there's mtb name with language", () => {
const results = GeoJSONUtils.getTitle({properties: { "mtb:name:he": "name"}} as any as GeoJSON.Feature, "he");
expect(results).toBe("name");
});

it("should get title when there's mtb name without language", () => {
const results = GeoJSONUtils.getTitle({properties: { "mtb:name": "name"}} as any as GeoJSON.Feature, "he");
expect(results).toBe("name");
});

it("should get title even when there's no title for language description", () => {
const results = GeoJSONUtils.getTitle({properties: { name: "name"}} as any as GeoJSON.Feature, "he");
expect(results).toBe("name");
});

it("should get title even when there's no title for language description", () => {
const results = GeoJSONUtils.getTitle({properties: { name: "name"}} as any as GeoJSON.Feature, "he");
expect(results).toBe("name");
});
it("should return has extra data for feature with description", () => {
expect(GeoJSONUtils.hasExtraData({properties: { "description:he": "desc"}} as any as GeoJSON.Feature, "he")).toBeTruthy();
});

it("should return has extra data for feature with image", () => {
expect(GeoJSONUtils.hasExtraData({properties: { image: "image-url"}} as any as GeoJSON.Feature, "he")).toBeTruthy();
});
});
71 changes: 71 additions & 0 deletions IsraelHiking.Web/src/application/services/geojson-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Immutable } from "immer";
import { LatLngAlt } from "../models/models";

export class GeoJSONUtils {
public static setProperty(feature: GeoJSON.Feature, key: string, value: string): string {
if (!feature.properties[key]) {
feature.properties[key] = value;
return "";
}
let index = 1;
while (feature.properties[key + index]) {
index++;
}
feature.properties[key + index] = value;
return `${index}`;
}

public static setLocation(feature: GeoJSON.Feature, value: LatLngAlt) {
feature.properties.poiGeolocation = {
lat: value.lat,
lon: value.lng
};
}

public static setDescription(feature: GeoJSON.Feature, value: string, language: string) {
feature.properties["description:" + language] = value;
}

public static setTitle(feature: GeoJSON.Feature, value: string, language: string) {
feature.properties["name:" + language] = value;
}

public static getTitle(feature: Immutable<GeoJSON.Feature>, language: string): string {
if (feature.properties["name:" + language]) {
return feature.properties["name:" + language];
}
if (feature.properties.name) {
return feature.properties.name;
}
if (feature.properties["mtb:name:"+ language]) {
return feature.properties["mtb:name:"+ language];
}
if (feature.properties["mtb:name"]) {
return feature.properties["mtb:name"];
}
return "";
}

public static getDescription(feature: Immutable<GeoJSON.Feature>, language: string): string {
return feature.properties["description:" + language] || feature.properties.description;
}

public static getExternalDescription(feature: GeoJSON.Feature, language: string): string {
return feature.properties["poiExternalDescription:" + language] || feature.properties.poiExternalDescription;
}

public static getLocation(feature: GeoJSON.Feature): LatLngAlt {
return {
lat: feature.properties.poiGeolocation.lat,
lng: feature.properties.poiGeolocation.lon,
alt: feature.properties.poiAlt
};
}

public static hasExtraData(feature: GeoJSON.Feature, language: string): boolean {
return feature.properties["description:" + language] ||
Object.keys(feature.properties).find(k => k.startsWith("image")) != null ||
Object.keys(feature.properties).find(k => k.startsWith("wikipedia")) != null ||
Object.keys(feature.properties).find(k => k.startsWith("wikidata")) != null;
}
}
102 changes: 102 additions & 0 deletions IsraelHiking.Web/src/application/services/iNature.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { firstValueFrom, timeout } from "rxjs";

import { GeoJSONUtils } from "./geojson-utils";
import { Urls } from "../urls";

@Injectable()
export class INatureService {
private readonly API_URL = "https://inature.info/w/api.php";
private readonly TIMEOUT = 3000;

constructor(private readonly httpClient: HttpClient) {}

public async createFeatureFromPageId(pageId: string): Promise<GeoJSON.Feature> {
const address = this.getContnetRetrivalAddress(pageId, true);
const {content, title} = await this.getPageContentAndTitleFromAddress(address);
const feature: GeoJSON.Feature = {
type: "Feature",
properties: {},
geometry: {
type: "Point",
coordinates: []
}
};
const lngLat = this.setLocation(content, feature);
this.setImageAndWebsite(content, feature, pageId);
feature.geometry = await this.getGeometryFromContent(content) ?? {
type: "Point",
coordinates: [lngLat.lng, lngLat.lat]
};
feature.properties.poiSource = "iNature";
feature.properties.poiCategory = "iNature";
feature.properties.poiLanguage = "he";
feature.properties.poiId = "iNature_" + pageId;
feature.properties.identifier = pageId;
feature.properties.name = title;
if (feature.geometry.type === "LineString") {
feature.properties.icon = "icon-hiking";
feature.properties.poiCategory = "Hiking";
feature.properties.iconColor = "black";
feature.properties.name = feature.properties.name + " - טבע ונופים";
} else {
feature.properties.icon = "icon-nature";
feature.properties.poiCategory = "iNature";
feature.properties.iconColor = "#116C00";
}
return feature;
}

public async enritchFeatureFromINature(feature: GeoJSON.Feature): Promise<void> {
const iNatureRef = feature.properties["ref:IL:inature"];
const address = this.getContnetRetrivalAddress(iNatureRef, false);
const contentAndTitle = await this.getPageContentAndTitleFromAddress(address);
this.setImageAndWebsite(contentAndTitle.content, feature, iNatureRef);
}

private setLocation(content: string, feature: GeoJSON.Feature) {
const regexp = /נצ=(\d+\.\d+)\s*,\s*(\d+\.\d+)/;
const match = content.match(regexp);
const latLng = { lat: parseFloat(match[1]), lng: parseFloat(match[2]) };
GeoJSONUtils.setLocation(feature, latLng);
return latLng;
}

private setImageAndWebsite(content: string, feature: GeoJSON.Feature, title: string) {
feature.properties.poiExternalDescription = content.match(/סקירה=(.*)/)[1];
const indexString = GeoJSONUtils.setProperty(feature, "website", `https://inature.info/wiki/${title}`);
feature.properties["poiSourceImageUrl" + indexString] = "https://user-images.githubusercontent.com/3269297/37312048-2d6e7488-2652-11e8-9dbe-c1465ff2e197.png";
const image = content.match(/תמונה=(.*)/)[1];
const imageSrc = `https://inature.info/w/index.php?title=Special:Redirect/file/${image}`;
GeoJSONUtils.setProperty(feature, "image", imageSrc);
}

private getContnetRetrivalAddress(key: string, isPageId: boolean): string {
const baseAddress = `${this.API_URL}?action=query&prop=revisions&rvprop=content&format=json&origin=*`
if (isPageId) {
return baseAddress + `&pageids=${key}`;
}
return baseAddress + `&titles=${key}`;
}

private async getPageContentAndTitleFromAddress(address: string): Promise<{content: string, title: string}> {
const iNatureJson = await firstValueFrom(this.httpClient.get(address).pipe(timeout(this.TIMEOUT))) as any;
const pageData = iNatureJson.query.pages[Object.keys(iNatureJson.query.pages)[0]];
return {
content: pageData.revisions[0]["*"],
title: pageData.title
}
}

private async getGeometryFromContent(content: string): Promise<GeoJSON.Geometry> {
const shareRegexp = new RegExp("israelhiking\.osm\.org\.il/share/(.*?)");

Check failure on line 93 in IsraelHiking.Web/src/application/services/iNature.service.ts

View workflow job for this annotation

GitHub Actions / backend-and-frontend-tests

Unnecessary escape character: \.

Check failure on line 93 in IsraelHiking.Web/src/application/services/iNature.service.ts

View workflow job for this annotation

GitHub Actions / backend-and-frontend-tests

Unnecessary escape character: \.

Check failure on line 93 in IsraelHiking.Web/src/application/services/iNature.service.ts

View workflow job for this annotation

GitHub Actions / backend-and-frontend-tests

Unnecessary escape character: \.
const match = content.match(shareRegexp);
if (match == null) {
return null;
}
const shareId = match[1];
const geojson = await firstValueFrom(this.httpClient.get(`${Urls.urls}${shareId}?format=geojson`)) as GeoJSON.FeatureCollection;
return geojson.features.find(f => f.geometry.type === "LineString")?.geometry;
}
}
Loading

0 comments on commit 9cef37a

Please sign in to comment.