import { BBox } from "../../Logic/BBox" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import Combine from "../Base/Combine" import Title from "../Base/Title" import { Overpass } from "../../Logic/Osm/Overpass" import { Store, UIEventSource } from "../../Logic/UIEventSource" import Constants from "../../Models/Constants" import RelationsTracker from "../../Logic/Osm/RelationsTracker" import { VariableUiElement } from "../Base/VariableUIElement" import { FlowStep } from "./FlowStep" import Loading from "../Base/Loading" import { SubtleButton } from "../Base/SubtleButton" import Svg from "../../Svg" import { Utils } from "../../Utils" import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage" import Minimap from "../Base/Minimap" import BaseLayer from "../../Models/BaseLayer" import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" import Loc from "../../Models/Loc" import Attribution from "../BigComponents/Attribution" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import ValidatedTextField from "../Input/ValidatedTextField" import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" import import_candidate from "../../assets/layers/import_candidate/import_candidate.json" import { GeoOperations } from "../../Logic/GeoOperations" import FeatureInfoBox from "../Popup/FeatureInfoBox" import { ImportUtils } from "./ImportUtils" import Translations from "../i18n/Translations" import currentview from "../../assets/layers/current_view/current_view.json" import { CheckBox } from "../Input/Checkboxes" import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" import { Feature, FeatureCollection, Point } from "geojson" import DivContainer from "../Base/DivContainer" /** * Given the data to import, the bbox and the layer, will query overpass for similar items */ export default class ConflationChecker extends Combine implements FlowStep<{ features: Feature[]; theme: string }> { public readonly IsValid public readonly Value: Store<{ features: Feature[]; theme: string }> constructor( state, params: { bbox: BBox; layer: LayerConfig; theme: string; features: Feature[] } ) { const t = Translations.t.importHelper.conflationChecker const bbox = params.bbox.padAbsolute(0.0001) const layer = params.layer const toImport: { features: any[] } = params let overpassStatus = new UIEventSource< { error: string } | "running" | "success" | "idle" | "cached" >("idle") function loadDataFromOverpass() { // Load the data! const url = Constants.defaultOverpassUrls[1] const relationTracker = new RelationsTracker() const overpass = new Overpass( params.layer.source.osmTags, [], url, new UIEventSource(180), relationTracker, true ) console.log("Loading from overpass!") overpassStatus.setData("running") overpass.queryGeoJson(bbox).then( ([data, date]) => { console.log( "Received overpass-data: ", data.features.length, "features are loaded at ", date ) overpassStatus.setData("success") fromLocalStorage.setData([data, date]) }, (error) => { overpassStatus.setData({ error }) } ) } const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>( "importer-overpass-cache-" + layer.id, { whenLoaded: (v) => { if (v !== undefined && v !== null) { console.log("Loaded from local storage:", v) overpassStatus.setData("cached") } else { loadDataFromOverpass() } }, } ) const cacheAge = fromLocalStorage.map((d) => { if (d === undefined || d[1] === undefined) { return undefined } const [_, loadedDate] = d return (new Date().getTime() - loadedDate.getTime()) / 1000 }) cacheAge.addCallbackD((timeDiff) => { if (timeDiff < 24 * 60 * 60) { // Recently cached! overpassStatus.setData("cached") return } else { loadDataFromOverpass() } }) const geojson: Store = fromLocalStorage.map((d) => { if (d === undefined) { return undefined } return d[0] }) const background = new UIEventSource(AvailableBaseLayers.osmCarto) const location = new UIEventSource({ lat: 0, lon: 0, zoom: 1 }) const currentBounds = new UIEventSource(undefined) const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({ value: LocalStorageSource.GetParsed("importer-zoom-level", "0"), }) zoomLevel.SetClass("ml-1 border border-black") const osmLiveData = Minimap.createMiniMap({ allowMoving: true, location, background, bounds: currentBounds, attribution: new Attribution( location, state.osmConnection.userDetails, undefined, currentBounds ), }) osmLiveData.SetClass("w-full").SetStyle("height: 500px") const geojsonFeatures: Store = geojson.map( (geojson) => { if (geojson?.features === undefined) { return [] } const currentZoom = zoomLevel.GetValue().data const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom) if (currentZoom !== undefined && !zoomedEnough) { return [] } const bounds = osmLiveData.bounds.data if (bounds === undefined) { return geojson.features } return geojson.features.filter((f) => BBox.get(f).overlapsWith(bounds)) }, [osmLiveData.bounds, zoomLevel.GetValue()] ) const preview = StaticFeatureSource.fromGeojsonStore(geojsonFeatures) new ShowDataLayer({ layerToShow: new LayerConfig(currentview), state, leafletMap: osmLiveData.leafletMap, popup: undefined, zoomToFeatures: true, features: StaticFeatureSource.fromGeojson([bbox.asGeoJson({})]), }) new ShowDataLayer({ layerToShow: layer, state, leafletMap: osmLiveData.leafletMap, popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: false, features: preview, }) new ShowDataLayer({ layerToShow: new LayerConfig(import_candidate), state, leafletMap: osmLiveData.leafletMap, popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: false, features: StaticFeatureSource.fromGeojson(toImport.features), }) const nearbyCutoff = ValidatedTextField.ForType("pnat").ConstructInputElement() nearbyCutoff.SetClass("ml-1 border border-black") nearbyCutoff.GetValue().syncWith(LocalStorageSource.Get("importer-cutoff", "25"), true) const matchedFeaturesMap = Minimap.createMiniMap({ allowMoving: true, background, }) matchedFeaturesMap.SetClass("w-full").SetStyle("height: 500px") // Featuresource showing OSM-features which are nearby a toImport-feature const geojsonMapped: Store = geojson.map( (osmData) => { if (osmData?.features === undefined) { return [] } const maxDist = Number(nearbyCutoff.GetValue().data) return osmData.features.filter((f) => toImport.features.some( (imp) => maxDist >= GeoOperations.distanceBetween( imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f) ) ) ) }, [nearbyCutoff.GetValue().stabilized(500)] ) const nearbyFeatures = StaticFeatureSource.fromGeojsonStore(geojsonMapped) const paritionedImport = ImportUtils.partitionFeaturesIfNearby( toImport, geojson, nearbyCutoff.GetValue().map(Number) ) // Featuresource showing OSM-features which are nearby a toImport-feature const toImportWithNearby = StaticFeatureSource.fromGeojsonStore( paritionedImport.map((els) => els?.hasNearby ?? []) ) toImportWithNearby.features.addCallback((nearby) => console.log("The following features are near an already existing object:", nearby) ) new ShowDataLayer({ layerToShow: new LayerConfig(import_candidate), state, leafletMap: matchedFeaturesMap.leafletMap, popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: false, features: toImportWithNearby, }) const showOsmLayer = new CheckBox(t.showOsmLayerInConflationMap, true) new ShowDataLayer({ layerToShow: layer, state, leafletMap: matchedFeaturesMap.leafletMap, popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: true, features: nearbyFeatures, doShowLayer: showOsmLayer.GetValue(), }) const conflationMaps = new Combine([ new VariableUiElement( geojson.map((geojson) => { if (geojson === undefined) { return undefined } return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick( () => { Utils.offerContentsAsDownloadableFile( JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", { mimetype: "application/json+geo", } ) } ) }) ), new VariableUiElement( cacheAge.map((age) => { if (age === undefined) { return undefined } if (age < 0) { return t.cacheExpired } return new Combine([ t.loadedDataAge.Subs({ age: Utils.toHumanTime(age) }), new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache) .onClick(loadDataFromOverpass) .SetClass("h-12"), ]) }) ), new Title(t.titleLive), t.importCandidatesCount.Subs({ count: toImport.features.length }), new VariableUiElement( geojson.map((geojson) => { if ( geojson?.features?.length === undefined || geojson?.features?.length === 0 ) { return t.nothingLoaded.Subs(layer).SetClass("alert") } return new Combine([ t.osmLoaded.Subs({ count: geojson.features.length, name: layer.name }), ]) }) ), osmLiveData, new Combine([ t.zoomLevelSelection, zoomLevel, new VariableUiElement( osmLiveData.location.map((location) => { return t.zoomIn.Subs({ current: location.zoom }) }) ), ]).SetClass("flex"), new DivContainer("fullscreen"), new Title(t.titleNearby), new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"), new VariableUiElement( toImportWithNearby.features.map((feats) => t.nearbyWarn.Subs({ count: feats.length }).SetClass("alert") ) ), t.setRangeToZero, matchedFeaturesMap, new Combine([ new BackgroundMapSwitch( { backgroundLayer: background, locationControl: matchedFeaturesMap.location }, background ), showOsmLayer, ]).SetClass("flex"), ]).SetClass("flex flex-col") super([ new Title(t.title), new VariableUiElement( overpassStatus.map((d) => { if (d === "idle") { return new Loading(t.states.idle) } if (d === "running") { return new Loading(t.states.running) } if (d["error"] !== undefined) { return t.states.error.Subs({ error: d["error"] }).SetClass("alert") } if (d === "cached") { return conflationMaps } if (d === "success") { return conflationMaps } return t.states.unexpected.Subs({ state: d }).SetClass("alert") }) ), ]) this.Value = paritionedImport.map((feats) => ({ theme: params.theme, features: feats?.noNearby, layer: params.layer, })) this.IsValid = this.Value.map((v) => v?.features !== undefined && v.features.length > 0) } }