diff --git a/IsraelHiking.Web/src/application/components/map/layers-view.component.ts b/IsraelHiking.Web/src/application/components/map/layers-view.component.ts index d8ff9e4eb..ff69c66c4 100644 --- a/IsraelHiking.Web/src/application/components/map/layers-view.component.ts +++ b/IsraelHiking.Web/src/application/components/map/layers-view.component.ts @@ -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({ @@ -132,11 +133,11 @@ export class LayersViewComponent extends BaseMapComponent implements OnInit { } public getTitle(feature: GeoJSON.Feature): string { - 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 isCoordinatesFeature(feature: Immutable) { diff --git a/IsraelHiking.Web/src/application/components/overlays/cluster-overlay.component.ts b/IsraelHiking.Web/src/application/components/overlays/cluster-overlay.component.ts index c168dd9b1..79b5f4f84 100644 --- a/IsraelHiking.Web/src/application/components/overlays/cluster-overlay.component.ts +++ b/IsraelHiking.Web/src/application/components/overlays/cluster-overlay.component.ts @@ -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", @@ -20,18 +20,17 @@ export class ClusterOverlayComponent extends BaseMapComponent { public closed: EventEmitter; 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) { diff --git a/IsraelHiking.Web/src/application/components/sidebar/publicpoi/public-poi-sidebar.component.ts b/IsraelHiking.Web/src/application/components/sidebar/publicpoi/public-poi-sidebar.component.ts index 77cc9aa05..42a411901 100644 --- a/IsraelHiking.Web/src/application/components/sidebar/publicpoi/public-poi-sidebar.component.ts +++ b/IsraelHiking.Web/src/application/components/sidebar/publicpoi/public-poi-sidebar.component.ts @@ -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, @@ -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[] { @@ -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; } @@ -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); } diff --git a/IsraelHiking.Web/src/application/services/geojson-utils.spec.ts b/IsraelHiking.Web/src/application/services/geojson-utils.spec.ts new file mode 100644 index 000000000..13231691f --- /dev/null +++ b/IsraelHiking.Web/src/application/services/geojson-utils.spec.ts @@ -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(); + }); +}); \ No newline at end of file diff --git a/IsraelHiking.Web/src/application/services/geojson-utils.ts b/IsraelHiking.Web/src/application/services/geojson-utils.ts new file mode 100644 index 000000000..b7faef3ac --- /dev/null +++ b/IsraelHiking.Web/src/application/services/geojson-utils.ts @@ -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, 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, 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; + } +} \ No newline at end of file diff --git a/IsraelHiking.Web/src/application/services/iNature.service.ts b/IsraelHiking.Web/src/application/services/iNature.service.ts new file mode 100644 index 000000000..2d545ae4c --- /dev/null +++ b/IsraelHiking.Web/src/application/services/iNature.service.ts @@ -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 { + 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 { + 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 { + const shareRegexp = new RegExp("israelhiking\.osm\.org\.il/share/(.*?)"); + 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; + } +} \ No newline at end of file diff --git a/IsraelHiking.Web/src/application/services/poi.service.spec.ts b/IsraelHiking.Web/src/application/services/poi.service.spec.ts index 26a1081c3..04ea3f82b 100644 --- a/IsraelHiking.Web/src/application/services/poi.service.spec.ts +++ b/IsraelHiking.Web/src/application/services/poi.service.spec.ts @@ -2,6 +2,7 @@ import { TestBed, inject } from "@angular/core/testing"; import { HttpRequest, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; import { NgxsModule, Store } from "@ngxs/store"; +import { LngLatBounds } from "maplibre-gl"; import { ToastServiceMockCreator } from "./toast.service.spec"; import { ResourcesService } from "./resources.service"; @@ -21,8 +22,9 @@ import { Urls } from "../urls"; import { LayersReducer } from "../reducers/layers.reducer"; import { AddToPoiQueueAction, OfflineReducer } from "../reducers/offline.reducer"; import { ConfigurationReducer, SetLanguageAction } from "../reducers/configuration.reducer"; +import { GeoJSONUtils } from "./geojson-utils"; +import { INatureService } from "./inature.service"; import type { Category, MarkerData } from "../models/models"; -import { LngLatBounds } from "maplibre-gl"; describe("Poi Service", () => { @@ -75,6 +77,7 @@ describe("Poi Service", () => { { provide: MapService, useValue: mapServiceMock }, { provide: LoggingService, useValue: loggingService }, { provide: OverpassTurboService, useValue: {} }, + { provide: INatureService, useValue: {} }, ConnectionService, GeoJsonParser, RunningContextService, @@ -447,9 +450,9 @@ describe("Poi Service", () => { expect(feature.properties.poiId).not.toBeNull(); expect(feature.properties.poiSource).toBe("OSM"); expect(feature.properties["description:he"]).toBe("description"); - expect(poiService.getDescription(feature, "he")).toBe("description"); + expect(GeoJSONUtils.getDescription(feature, "he")).toBe("description"); expect(feature.properties["name:he"]).toBe("title"); - expect(poiService.getTitle(feature, "he")).toBe("title"); + expect(GeoJSONUtils.getTitle(feature, "he")).toBe("title"); expect(feature.properties.poiAddedUrls).toEqual(["some-new-url"]); expect(feature.properties.poiRemovedUrls).toEqual(["some-old-url"]); expect(feature.properties.poiAddedImages).toEqual(["some-new-image-url"]); @@ -457,8 +460,8 @@ describe("Poi Service", () => { expect(feature.properties.poiIcon).toBe("icon-spring"); expect(feature.properties.poiGeolocation.lat).toBe(1); expect(feature.properties.poiGeolocation.lon).toBe(2); - expect(poiService.getLocation(feature).lat).toBe(1); - expect(poiService.getLocation(feature).lng).toBe(2); + expect(GeoJSONUtils.getLocation(feature).lat).toBe(1); + expect(GeoJSONUtils.getLocation(feature).lng).toBe(2); // expected to not change geometry expect(feature.geometry.type).toBe("Point"); expect((feature.geometry as GeoJSON.Point).coordinates).toEqual([0, 0]); @@ -532,7 +535,7 @@ describe("Poi Service", () => { coordinates: [0, 0] } } as GeoJSON.Feature; - poiService.setLocation(featureInQueue, { lat: 1, lng: 2 }); + GeoJSONUtils.setLocation(featureInQueue, { lat: 1, lng: 2 }); dbMock.getPoiFromUploadQueue = () => Promise.resolve(featureInQueue); store.reset({ poiState: { @@ -573,16 +576,16 @@ describe("Poi Service", () => { expect(feature.properties.poiId).not.toBeNull(); expect(feature.properties.poiSource).toBe("OSM"); expect(feature.properties["description:he"]).toBe("description"); - expect(poiService.getDescription(feature, "he")).toBe("description"); + expect(GeoJSONUtils.getDescription(feature, "he")).toBe("description"); expect(feature.properties["name:he"]).toBe("title"); - expect(poiService.getTitle(feature, "he")).toBe("title"); + expect(GeoJSONUtils.getTitle(feature, "he")).toBe("title"); expect(feature.properties.poiAddedUrls).toEqual(["some-url"]); expect(feature.properties.poiAddedImages).toEqual(["some-image-url"]); expect(feature.properties.poiIcon).toBeUndefined(); expect(feature.properties.poiGeolocation.lat).toBe(1); expect(feature.properties.poiGeolocation.lon).toBe(2); - expect(poiService.getLocation(feature).lat).toBe(1); - expect(poiService.getLocation(feature).lng).toBe(2); + expect(GeoJSONUtils.getLocation(feature).lat).toBe(1); + expect(GeoJSONUtils.getLocation(feature).lng).toBe(2); // expected to not change geometry expect(feature.geometry.type).toBe("Point"); expect((feature.geometry as GeoJSON.Point).coordinates).toEqual([0, 0]); @@ -618,11 +621,11 @@ describe("Poi Service", () => { poiService.mergeWithPoi(feature, markerData); const info = poiService.getEditableDataFromFeature(feature); const featureAfterConverstion = poiService.getFeatureFromEditableData(info); - poiService.setLocation(featureAfterConverstion, { lat: 2, lng: 1}); - expect(poiService.getLocation(featureAfterConverstion).lat).toBe(2); - expect(poiService.getLocation(featureAfterConverstion).lng).toBe(1); - expect(poiService.getDescription(featureAfterConverstion, "he")).toBe("description"); - expect(poiService.getTitle(featureAfterConverstion, "he")).toBe("title"); + GeoJSONUtils.setLocation(featureAfterConverstion, { lat: 2, lng: 1}); + expect(GeoJSONUtils.getLocation(featureAfterConverstion).lat).toBe(2); + expect(GeoJSONUtils.getLocation(featureAfterConverstion).lng).toBe(1); + expect(GeoJSONUtils.getDescription(featureAfterConverstion, "he")).toBe("description"); + expect(GeoJSONUtils.getTitle(featureAfterConverstion, "he")).toBe("title"); expect(featureAfterConverstion.properties.image).toBe("image-url"); expect(featureAfterConverstion.properties.image0).toBeUndefined(); expect(featureAfterConverstion.properties.poiIcon).toBe("icon-some-type"); @@ -662,14 +665,6 @@ describe("Poi Service", () => { }) )); - it("should return has extra data for feature with description", inject([PoiService], (poiService: PoiService) => { - expect(poiService.hasExtraData({properties: { "description:he": "desc"}} as any as GeoJSON.Feature, "he")).toBeTruthy(); - })); - - it("should return has extra data for feature with image", inject([PoiService], (poiService: PoiService) => { - expect(poiService.hasExtraData({properties: { image: "image-url"}} as any as GeoJSON.Feature, "he")).toBeTruthy(); - })); - it("should return the itm coordinates for feature", inject([PoiService], (poiService: PoiService) => { const results = poiService.getItmCoordinates({properties: { poiItmEast: 1, poiItmNorth: 2}} as any as GeoJSON.Feature); expect(results.east).toBe(1); @@ -685,37 +680,7 @@ describe("Poi Service", () => { expect(results.userName).toBe("name"); })); - it("should get extenal description for hebrew", inject([PoiService], (poiService: PoiService) => { - const results = poiService.getExternalDescription( - {properties: { "poiExternalDescription:he": "desc"}} as any as GeoJSON.Feature, "he"); - expect(results).toBe("desc"); - })); - - it("should get extenal description for language independant", inject([PoiService], (poiService: PoiService) => { - const results = poiService.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", inject([PoiService], (poiService: PoiService) => { - const results = poiService.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", inject([PoiService], (poiService: PoiService) => { - const results = poiService.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", inject([PoiService], (poiService: PoiService) => { - const results = poiService.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", inject([PoiService], (poiService: PoiService) => { - const results = poiService.getTitle({properties: { name: "name"}} as any as GeoJSON.Feature, "he"); - expect(results).toBe("name"); - })); + it("should get social links", inject([PoiService], (poiService: PoiService) => { const results = poiService.getFeatureFromCoordinatesId("1_2", "he"); diff --git a/IsraelHiking.Web/src/application/services/poi.service.ts b/IsraelHiking.Web/src/application/services/poi.service.ts index 25087be11..5b4e5eb8c 100644 --- a/IsraelHiking.Web/src/application/services/poi.service.ts +++ b/IsraelHiking.Web/src/application/services/poi.service.ts @@ -39,6 +39,8 @@ import type { EditablePublicPointData, OfflineState } from "../models/models"; +import { GeoJSONUtils } from "./geojson-utils"; +import { INatureService } from "./inature.service"; export type SimplePointType = "Tap" | "CattleGrid" | "Parking" | "OpenGate" | "ClosedGate" | "Block" | "PicnicSite" @@ -120,6 +122,7 @@ export class PoiService { private readonly mapService: MapService, private readonly connectionService: ConnectionService, private readonly overpassTurboService: OverpassTurboService, + private readonly iNatureService: INatureService, private readonly store: Store ) { this.poisCache = []; @@ -231,7 +234,7 @@ export class PoiService { const poi = await firstValueFrom(poi$) as GeoJSON.Feature; if (feature.properties.poiIsSimple) { this.loggingService.info("[POIs] Uploaded successfully a simple feature with generated id: " + - `${firstItemId} at: ${JSON.stringify(this.getLocation(feature))}, removing from upload queue`); + `${firstItemId} at: ${JSON.stringify(GeoJSONUtils.getLocation(feature))}, removing from upload queue`); } else { this.loggingService.info("[POIs] Uploaded successfully a feature with id:" + `${this.getFeatureId(poi) ?? firstItemId}, removing from upload queue`); @@ -562,7 +565,7 @@ export class PoiService { if (visibleCategories.indexOf(feature.properties.poiCategory) === -1) { continue; } - if (this.getTitle(feature, language) || this.hasExtraData(feature, language)) { + if (GeoJSONUtils.getTitle(feature, language) || GeoJSONUtils.hasExtraData(feature, language)) { visibleFeatures.push(feature); } } @@ -658,10 +661,10 @@ export class PoiService { const languageShort = language || this.resources.getCurrentLanguageCodeSimplified(); const title = wikidata.sitelinks[`${languageShort}wiki`]?.title; if (wikidata.statements.P18 && wikidata.statements.P18.length > 0) { - this.setProperty(feature, "image", `File:${wikidata.statements.P18[0].value.content}`); + GeoJSONUtils.setProperty(feature, "image", `File:${wikidata.statements.P18[0].value.content}`); } if (title) { - const indexString = this.setProperty(feature, "website", `https://${languageShort}.wikipedia.org/wiki/${title}`); + const indexString = GeoJSONUtils.setProperty(feature, "website", `https://${languageShort}.wikipedia.org/wiki/${title}`); feature.properties["poiSourceImageUrl" + indexString] = "https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg/128px-Wikipedia-logo-v2.svg.png"; } const wikipediaPage = await firstValueFrom(this.httpClient.get(`http://${languageShort}.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles=${title}&origin=*`)) as any; @@ -671,19 +674,6 @@ export class PoiService { } } - private async enritchFeatureFromINature(feature: GeoJSON.Feature): Promise { - const iNatureRef = feature.properties["ref:IL:inature"]; - const address = `https://inature.info/w/api.php?action=query&prop=revisions&rvprop=content&format=json&titles=${iNatureRef}&origin=*`; - const iNatureJson = await firstValueFrom(this.httpClient.get(address).pipe(timeout(3000))) as any; - const content = iNatureJson.query.pages[Object.keys(iNatureJson.query.pages)[0]].revisions[0]["*"]; - feature.properties.poiExternalDescription = content.match(/סקירה=(.*)/)[1]; - const indexString = this.setProperty(feature, "website", `https://inature.info/wiki/${iNatureRef}`); - 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}`; - this.setProperty(feature, "image", imageSrc); - } - public async getPoint(id: string, source: string, language?: string): Promise { const itemInCache = this.poisCache.find(f => this.getFeatureId(f) === id && f.properties.source === source); if (itemInCache) { @@ -707,7 +697,7 @@ export class PoiService { wikidataPromise = this.enritchFeatureFromWikimedia(feature, language); } if (feature.properties["ref:IL:inature"] && language === "he") { - inaturePromise = this.enritchFeatureFromINature(feature); + inaturePromise = this.iNatureService.enritchFeatureFromINature(feature); } if (type === "node" && feature.properties.place) { placePromise = this.overpassTurboService.getPlaceGeometry(osmId); @@ -731,6 +721,13 @@ export class PoiService { this.adjustGeolocationBasedOnTileDate(id, poi); this.poisCache.splice(0, 0, poi); return cloneDeep(poi); + } else if (source === "iNature") { + const feature = await this.iNatureService.createFeatureFromPageId(id); + this.poisCache.splice(0, 0, feature); + return cloneDeep(feature); + // HM TODO: add support for wikidata + //} else if (source === "wikidata") { + } else { const params = new HttpParams().set("language", language || this.resources.getCurrentLanguageCodeSimplified()); const poi$ = this.httpClient.get(Urls.poi + source + "/" + id, { params }).pipe(timeout(6000)); @@ -775,8 +772,8 @@ export class PoiService { coordinates: SpatialService.toCoordinate(latlng) } } as GeoJSON.Feature; - this.setLocation(feature, latlng); - this.setTitle(feature, id, language); + GeoJSONUtils.setLocation(feature, latlng); + GeoJSONUtils.setTitle(feature, id, language); return feature; } @@ -795,20 +792,20 @@ export class PoiService { language } as PoiRouterData); const escaped = encodeURIComponent(poiLink); - const location = this.getLocation(feature); + const location = GeoJSONUtils.getLocation(feature); return { poiLink, facebook: `${Urls.facebook}${escaped}`, - whatsapp: this.whatsappService.getUrl(this.getTitle(feature, language), escaped) as string, + whatsapp: this.whatsappService.getUrl(GeoJSONUtils.getTitle(feature, language), escaped) as string, waze: `${Urls.waze}${location.lat},${location.lng}` }; } public mergeWithPoi(feature: GeoJSON.Feature, markerData: Immutable) { const language = this.resources.getCurrentLanguageCodeSimplified(); - this.setTitle(feature, feature.properties["name:" + language] || markerData.title, language); - this.setDescription(feature, feature.properties["description:" + language] || markerData.description, language); - this.setLocation(feature, markerData.latlng); + GeoJSONUtils.setTitle(feature, feature.properties["name:" + language] || markerData.title, language); + GeoJSONUtils.setDescription(feature, feature.properties["description:" + language] || markerData.description, language); + GeoJSONUtils.setLocation(feature, markerData.latlng); feature.properties.poiIcon = feature.properties.poiIcon || `icon-${markerData.type || "star"}`; let lastIndex = Math.max(-1, ...Object.keys(feature.properties) .filter(k => k.startsWith("image")) @@ -823,53 +820,6 @@ export class PoiService { return feature; } - public setDescription(feature: GeoJSON.Feature, value: string, language: string) { - feature.properties["description:" + language] = value; - } - - public setTitle(feature: GeoJSON.Feature, value: string, language: string) { - feature.properties["name:" + language] = value; - } - - public setLocation(feature: GeoJSON.Feature, value: LatLngAlt) { - feature.properties.poiGeolocation = { - lat: value.lat, - lon: value.lng - }; - } - - public getTitle(feature: Immutable, 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 getDescription(feature: Immutable, language: string): string { - return feature.properties["description:" + language] || feature.properties.description; - } - - public getExternalDescription(feature: GeoJSON.Feature, language: string): string { - return feature.properties["poiExternalDescription:" + language] || feature.properties.poiExternalDescription; - } - - public getLocation(feature: GeoJSON.Feature): LatLngAlt { - return { - lat: feature.properties.poiGeolocation.lat, - lng: feature.properties.poiGeolocation.lon, - alt: feature.properties.poiAlt - }; - } - public getContribution(feature: GeoJSON.Feature): Contribution { return { lastModifiedDate: new Date(feature.properties.poiLastModified), @@ -885,13 +835,6 @@ export class PoiService { } as NorthEast; } - public 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; - } - public async getClosestPoint(location: LatLngAlt, source?: string, language?: string): Promise { let feature = null; try { @@ -926,13 +869,13 @@ export class PoiService { coordinates: SpatialService.toCoordinate(latlng) }, } as GeoJSON.Feature; - this.setLocation(feature, latlng); + GeoJSONUtils.setLocation(feature, latlng); return this.addPointToUploadQueue(feature); } public addComplexPoi(info: EditablePublicPointData, location: LatLngAlt): Promise { const feature = this.getFeatureFromEditableData(info); - this.setLocation(feature, location); + GeoJSONUtils.setLocation(feature, location); const id = uuidv4(); feature.id = id; feature.properties.poiId = id; @@ -970,16 +913,16 @@ export class PoiService { } if (newLocation) { - this.setLocation(featureContainingOnlyChanges, newLocation); + GeoJSONUtils.setLocation(featureContainingOnlyChanges, newLocation); hasChages = true; } const language = this.resources.getCurrentLanguageCodeSimplified(); if (info.title !== editableDataBeforeChanges.title) { - this.setTitle(featureContainingOnlyChanges, info.title, language); + GeoJSONUtils.setTitle(featureContainingOnlyChanges, info.title, language); hasChages = true; } if (info.description !== editableDataBeforeChanges.description) { - this.setDescription(featureContainingOnlyChanges, info.description, language); + GeoJSONUtils.setDescription(featureContainingOnlyChanges, info.description, language); hasChages = true; } if (info.icon !== editableDataBeforeChanges.icon || info.iconColor !== editableDataBeforeChanges.iconColor) { @@ -1021,8 +964,8 @@ export class PoiService { return { id: this.getFeatureId(feature), category: feature.properties.poiCategory, - description: this.getDescription(feature, language), - title: this.getTitle(feature, language), + description: GeoJSONUtils.getDescription(feature, language), + title: GeoJSONUtils.getTitle(feature, language), icon: feature.properties.poiIcon, iconColor: feature.properties.poiIconColor, imagesUrls: Object.keys(feature.properties).filter(k => k.startsWith("image")).map(k => feature.properties[k]), @@ -1059,8 +1002,8 @@ export class PoiService { index++; } const language = this.resources.getCurrentLanguageCodeSimplified(); - this.setDescription(feature, info.description, language); - this.setTitle(feature, info.title, language); + GeoJSONUtils.setDescription(feature, info.description, language); + GeoJSONUtils.setTitle(feature, info.title, language); return feature; } @@ -1070,17 +1013,4 @@ export class PoiService { } return feature.id ?? feature.properties.poiId; } - - private 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}`; - } }