forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			359 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			359 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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 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 { 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<Point>[]; theme: string }>
 | |
| {
 | |
|     public readonly IsValid
 | |
|     public readonly Value: Store<{ features: Feature<Point>[]; theme: string }>
 | |
| 
 | |
|     constructor(
 | |
|         state,
 | |
|         params: { bbox: BBox; layer: LayerConfig; theme: string; features: Feature<Point>[] }
 | |
|     ) {
 | |
|         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<number>(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<FeatureCollection> = fromLocalStorage.map((d) => {
 | |
|             if (d === undefined) {
 | |
|                 return undefined
 | |
|             }
 | |
|             return d[0]
 | |
|         })
 | |
| 
 | |
|         const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
 | |
|         const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
 | |
|         const currentBounds = new UIEventSource<BBox>(undefined)
 | |
|         const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({
 | |
|             value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0"),
 | |
|         })
 | |
|         zoomLevel.SetClass("ml-1 border border-black")
 | |
|         const osmLiveData = Minimap.createMiniMap({
 | |
|             allowMoving: true,
 | |
|             location,
 | |
|             background,
 | |
|             bounds: currentBounds,
 | |
|         })
 | |
|         osmLiveData.SetClass("w-full").SetStyle("height: 500px")
 | |
| 
 | |
|         const geojsonFeatures: Store<Feature[]> = 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 = new StaticFeatureSource(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<Feature[]> = 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 = new StaticFeatureSource(geojsonMapped)
 | |
|         const paritionedImport = ImportUtils.partitionFeaturesIfNearby(
 | |
|             toImport,
 | |
|             geojson,
 | |
|             nearbyCutoff.GetValue().map(Number)
 | |
|         )
 | |
| 
 | |
|         // Featuresource showing OSM-features which are nearby a toImport-feature
 | |
|         const toImportWithNearby = new StaticFeatureSource(
 | |
|             paritionedImport.map((els) => <Feature[]>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(<any>{ 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,
 | |
|             showOsmLayer,
 | |
|         ]).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: <any>feats?.noNearby,
 | |
|             layer: params.layer,
 | |
|         }))
 | |
|         this.IsValid = this.Value.map((v) => v?.features !== undefined && v.features.length > 0)
 | |
|     }
 | |
| }
 |