forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			321 lines
		
	
	
		
			No EOL
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			321 lines
		
	
	
		
			No EOL
		
	
	
		
			13 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 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 * as 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 ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
 | |
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
 | |
| import {Feature, FeatureCollection} from "@turf/turf";
 | |
| import * as currentview from "../../assets/layers/current_view/current_view.json"
 | |
| import {CheckBox} from "../Input/Checkboxes";
 | |
| import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch";
 | |
| 
 | |
| /**
 | |
|  * 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: any[], theme: string }> {
 | |
| 
 | |
|     public readonly IsValid
 | |
|     public readonly Value: Store<{ features: any[], theme: string }>
 | |
|     
 | |
|     constructor(
 | |
|         state,
 | |
|         params: { bbox: BBox, layer: LayerConfig, theme: string, features: any[] }) {
 | |
| 
 | |
|         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,
 | |
|             attribution: new Attribution(location, state.osmConnection.userDetails, undefined, 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 = StaticFeatureSource.fromGeojsonStore(geojsonFeatures)
 | |
| 
 | |
|         new ShowDataLayer({
 | |
|             layerToShow: new LayerConfig(currentview),
 | |
|             state,
 | |
|             leafletMap: osmLiveData.leafletMap,
 | |
|             popup: undefined,
 | |
|             zoomToFeatures: true,
 | |
|             features: StaticFeatureSource.fromGeojson([
 | |
|                 bbox.asGeoJson({})
 | |
|             ])
 | |
|         })
 | |
| 
 | |
|         new ShowDataMultiLayer({
 | |
|             //layerToShow: layer,
 | |
|             layers: new UIEventSource<FilteredLayer[]>([{
 | |
|                 layerDef: layer,
 | |
|                 isDisplayed: new UIEventSource<boolean>(true),
 | |
|                 appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
 | |
|             }]),
 | |
|             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 = 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(<any>{current: location.zoom})
 | |
|                 })),
 | |
|             ]).SetClass("flex"),
 | |
|             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)
 | |
|     }
 | |
| 
 | |
| } |