forked from MapComplete/MapComplete
Finish importer, add applicable import layers to every theme by default
This commit is contained in:
parent
3402ac0954
commit
ca1490902c
41 changed files with 1559 additions and 898 deletions
100
UI/ImportFlow/AskMetadata.ts
Normal file
100
UI/ImportFlow/AskMetadata.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {FlowStep} from "./FlowStep";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
|
||||
import Title from "../Base/Title";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class AskMetadata extends Combine implements FlowStep<{
|
||||
features: any[],
|
||||
wikilink: string,
|
||||
intro: string,
|
||||
source: string,
|
||||
theme: string
|
||||
}> {
|
||||
|
||||
public readonly Value: UIEventSource<{
|
||||
features: any[],
|
||||
wikilink: string,
|
||||
intro: string,
|
||||
source: string,
|
||||
theme: string
|
||||
}>;
|
||||
public readonly IsValid: UIEventSource<boolean>;
|
||||
|
||||
constructor(params: ({ features: any[], layer: LayerConfig })) {
|
||||
|
||||
const introduction = ValidatedTextField.InputForType("text", {
|
||||
value: LocalStorageSource.Get("import-helper-introduction-text"),
|
||||
inputStyle: "width: 100%"
|
||||
})
|
||||
|
||||
const wikilink = ValidatedTextField.InputForType("string", {
|
||||
value: LocalStorageSource.Get("import-helper-wikilink-text"),
|
||||
inputStyle: "width: 100%"
|
||||
})
|
||||
|
||||
const source = ValidatedTextField.InputForType("string", {
|
||||
value: LocalStorageSource.Get("import-helper-source-text"),
|
||||
inputStyle: "width: 100%"
|
||||
})
|
||||
|
||||
let options : {value: string, shown: BaseUIElement}[]= AllKnownLayouts.layoutsList
|
||||
.filter(th => th.layers.some(l => l.id === params.layer.id))
|
||||
.filter(th => th.id !== "personal")
|
||||
.map(th => ({
|
||||
value: th.id,
|
||||
shown: th.title
|
||||
}))
|
||||
|
||||
options.splice(0,0, {
|
||||
shown: new FixedUiElement("Select a theme"),
|
||||
value: undefined
|
||||
})
|
||||
|
||||
const theme = new DropDown("Which theme should be linked in the note?",options)
|
||||
|
||||
ValidatedTextField.InputForType("string", {
|
||||
value: LocalStorageSource.Get("import-helper-theme-text"),
|
||||
inputStyle: "width: 100%"
|
||||
})
|
||||
|
||||
super([
|
||||
new Title("Set metadata"),
|
||||
"Before adding " + params.features.length + " notes, please provide some extra information.",
|
||||
"Please, write an introduction for someone who sees the note",
|
||||
introduction.SetClass("w-full border border-black"),
|
||||
"What is the source of this data? If 'source' is set in the feature, this value will be ignored",
|
||||
source.SetClass("w-full border border-black"),
|
||||
"On what wikipage can one find more information about this import?",
|
||||
wikilink.SetClass("w-full border border-black"),
|
||||
theme
|
||||
]);
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
this.Value = introduction.GetValue().map(intro => {
|
||||
return {
|
||||
features: params.features,
|
||||
wikilink: wikilink.GetValue().data,
|
||||
intro,
|
||||
source: source.GetValue().data,
|
||||
theme: theme.GetValue().data
|
||||
|
||||
}
|
||||
}, [wikilink.GetValue(), source.GetValue(), theme.GetValue()])
|
||||
|
||||
this.IsValid = this.Value.map(obj => {
|
||||
if(obj === undefined){
|
||||
return false;
|
||||
}
|
||||
return obj.theme !== undefined && obj.features !== undefined && obj.wikilink !== undefined && obj.intro !== undefined && obj.source !== undefined;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
134
UI/ImportFlow/CompareToAlreadyExistingNotes.ts
Normal file
134
UI/ImportFlow/CompareToAlreadyExistingNotes.ts
Normal file
|
@ -0,0 +1,134 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {FlowStep} from "./FlowStep";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {DesugaringContext} from "../../Models/ThemeConfig/Conversion/Conversion";
|
||||
import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
|
||||
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||
import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource";
|
||||
import MetaTagging from "../../Logic/MetaTagging";
|
||||
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
|
||||
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource";
|
||||
import Minimap from "../Base/Minimap";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
import {ImportUtils} from "./ImportUtils";
|
||||
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import Title from "../Base/Title";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Loading from "../Base/Loading";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import * as known_layers from "../../assets/generated/known_layers.json"
|
||||
import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
|
||||
/**
|
||||
* Filters out points for which the import-note already exists, to prevent duplicates
|
||||
*/
|
||||
export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, geojson: any }> {
|
||||
|
||||
public IsValid: UIEventSource<boolean>
|
||||
public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, geojson: any }>
|
||||
|
||||
|
||||
constructor(state, params: { bbox: BBox, layer: LayerConfig, geojson: { features: any[] } }) {
|
||||
|
||||
const convertState: DesugaringContext = {
|
||||
sharedLayers: new Map(),
|
||||
tagRenderings: new Map()
|
||||
}
|
||||
|
||||
const layerConfig = known_layers.filter(l => l.id === params.layer.id)[0]
|
||||
const importLayerJson = new CreateNoteImportLayer(365).convertStrict(convertState, <LayerConfigJson> layerConfig, "CompareToAlreadyExistingNotes")
|
||||
const importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic")
|
||||
const flayer: FilteredLayer = {
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()),
|
||||
isDisplayed: new UIEventSource<boolean>(true),
|
||||
layerDef: importLayer
|
||||
}
|
||||
const unfiltered = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
|
||||
unfiltered.features.map(f => MetaTagging.addMetatags(
|
||||
f,
|
||||
{
|
||||
memberships: new RelationsTracker(),
|
||||
getFeaturesWithin: (layerId, bbox: BBox) => [],
|
||||
getFeatureById: (id: string) => undefined
|
||||
},
|
||||
importLayer,
|
||||
state,
|
||||
{
|
||||
includeDates: true,
|
||||
// We assume that the non-dated metatags are already set by the cache generator
|
||||
includeNonDates: true
|
||||
}
|
||||
)
|
||||
)
|
||||
const data = new FilteringFeatureSource(state, undefined, unfiltered)
|
||||
data.features.addCallbackD(features => console.log("Loaded and filtered features are", features))
|
||||
const map = Minimap.createMiniMap()
|
||||
map.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
const comparison = Minimap.createMiniMap({
|
||||
location: map.location,
|
||||
|
||||
})
|
||||
comparison.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: importLayer,
|
||||
state,
|
||||
zoomToFeatures: true,
|
||||
leafletMap: map.leafletMap,
|
||||
features: data,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state)
|
||||
})
|
||||
|
||||
|
||||
const maxDistance = new UIEventSource<number>(5)
|
||||
|
||||
const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(params.geojson, data.features
|
||||
.map(ff => ({features: ff.map(ff => ff.feature)})), maxDistance)
|
||||
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: new LayerConfig(import_candidate),
|
||||
state,
|
||||
zoomToFeatures: true,
|
||||
leafletMap: comparison.leafletMap,
|
||||
features: new StaticFeatureSource(partitionedImportPoints.map(p => p.hasNearby), false),
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state)
|
||||
})
|
||||
|
||||
super([
|
||||
new Title("Compare with already existing 'to-import'-notes"),
|
||||
new Toggle(
|
||||
new Loading("Fetching notes from OSM"),
|
||||
new Combine([
|
||||
map,
|
||||
"The following (red) elements are elements to import which are nearby a matching element that is already up for import. These won't be imported",
|
||||
|
||||
new Toggle(
|
||||
new FixedUiElement("All of the proposed points have (or had) an import note already").SetClass("alert w-full block").SetStyle("padding: 0.5rem"),
|
||||
new VariableUiElement(partitionedImportPoints.map(({noNearby}) => noNearby.length + " elements can be imported")).SetClass("thanks p-8"),
|
||||
partitionedImportPoints.map(({noNearby}) => noNearby.length === 0)
|
||||
).SetClass("w-full"),
|
||||
comparison,
|
||||
]).SetClass("flex flex-col"),
|
||||
unfiltered.features.map(ff => ff === undefined || ff.length === 0)
|
||||
),
|
||||
|
||||
|
||||
]);
|
||||
this.SetClass("flex flex-col")
|
||||
this.Value = partitionedImportPoints.map(({noNearby}) => ({
|
||||
geojson: {features: noNearby, type: "FeatureCollection"},
|
||||
bbox: params.bbox,
|
||||
layer: params.layer
|
||||
}))
|
||||
|
||||
this.IsValid = data.features.map(ff => ff.length > 0 && partitionedImportPoints.data.noNearby.length > 0, [partitionedImportPoints])
|
||||
}
|
||||
|
||||
}
|
32
UI/ImportFlow/ConfirmProcess.ts
Normal file
32
UI/ImportFlow/ConfirmProcess.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {FlowStep} from "./FlowStep";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Link from "../Base/Link";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import CheckBoxes from "../Input/Checkboxes";
|
||||
import Title from "../Base/Title";
|
||||
|
||||
export class ConfirmProcess<T> extends Combine implements FlowStep<T> {
|
||||
|
||||
public IsValid: UIEventSource<boolean>
|
||||
public Value: UIEventSource<T>
|
||||
|
||||
constructor(v: T) {
|
||||
|
||||
const toConfirm = [
|
||||
new Combine(["I have read the ", new Link("import guidelines on the OSM wiki", "https://wiki.openstreetmap.org/wiki/Import_guidelines", true)]),
|
||||
new FixedUiElement("I did contact the (local) community about this import"),
|
||||
new FixedUiElement("The license of the data to import allows it to be imported into OSM. They are allowed to be redistributed commercially, with only minimal attribution"),
|
||||
new FixedUiElement("The process is documented on the OSM-wiki (you'll need this link later)")
|
||||
];
|
||||
|
||||
const licenseClear = new CheckBoxes(toConfirm)
|
||||
super([
|
||||
new Title("Did you go through the import process?"),
|
||||
licenseClear
|
||||
]);
|
||||
this.SetClass("link-underline")
|
||||
this.IsValid = licenseClear.GetValue().map(selected => toConfirm.length == selected.length)
|
||||
this.Value = new UIEventSource<T>(v)
|
||||
}
|
||||
}
|
|
@ -27,10 +27,12 @@ import * as currentview from "../../assets/layers/current_view/current_view.json
|
|||
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";
|
||||
|
||||
/**
|
||||
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
||||
*/
|
||||
export default class ConflationChecker extends Combine implements FlowStep<any> {
|
||||
export default class ConflationChecker extends Combine implements FlowStep<{features: any[], layer: LayerConfig}> {
|
||||
|
||||
public readonly IsValid
|
||||
public readonly Value
|
||||
|
@ -44,19 +46,21 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
const layer = params.layer;
|
||||
const toImport = params.geojson;
|
||||
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle")
|
||||
|
||||
const cacheAge = new UIEventSource<number>(undefined);
|
||||
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
|
||||
whenLoaded: (v) => {
|
||||
if (v !== undefined) {
|
||||
console.log("Loaded from local storage:", v)
|
||||
const [geojson, date] = v;
|
||||
const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
|
||||
console.log("The cache is ", timeDiff, "seconds old")
|
||||
console.log("Loaded ", geojson.features.length," features; cache is ", timeDiff, "seconds old")
|
||||
cacheAge.setData(timeDiff)
|
||||
if (timeDiff < 24 * 60 * 60) {
|
||||
// Recently cached!
|
||||
overpassStatus.setData("cached")
|
||||
return;
|
||||
}
|
||||
cacheAge.setData(-1)
|
||||
}
|
||||
// Load the data!
|
||||
const url = Constants.defaultOverpassUrls[1]
|
||||
|
@ -115,7 +119,7 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
layerToShow:new LayerConfig(currentview),
|
||||
state,
|
||||
leafletMap: osmLiveData.leafletMap,
|
||||
enablePopups: undefined,
|
||||
popup: undefined,
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource([
|
||||
bbox.asGeoJson({})
|
||||
|
@ -161,17 +165,10 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
toImport.features.some(imp =>
|
||||
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
|
||||
}, [nearbyCutoff.GetValue()]), false);
|
||||
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
|
||||
|
||||
// Featuresource showing OSM-features which are nearby a toImport-feature
|
||||
const toImportWithNearby = new StaticFeatureSource(geojson.map(osmData => {
|
||||
if(osmData?.features === undefined){
|
||||
return []
|
||||
}
|
||||
const maxDist = Number(nearbyCutoff.GetValue().data)
|
||||
return toImport.features.filter(imp =>
|
||||
osmData.features.some(f =>
|
||||
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
|
||||
}, [nearbyCutoff.GetValue()]), false);
|
||||
const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els =>els?.hasNearby ?? []), false);
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow:layer,
|
||||
|
@ -192,6 +189,38 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
})
|
||||
|
||||
|
||||
const conflationMaps = new Combine([
|
||||
new VariableUiElement(
|
||||
geojson.map(geojson => {
|
||||
if (geojson === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").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 new FixedUiElement("Cache was expired")
|
||||
}
|
||||
return new FixedUiElement("Loaded data is from the cache and is "+Utils.toHumanTime(age)+" old")
|
||||
})),
|
||||
|
||||
new Title("Live data on OSM"),
|
||||
osmLiveData,
|
||||
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"),
|
||||
|
||||
new Title("Nearby features"),
|
||||
new Combine([ "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
|
||||
new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"),
|
||||
"Set the range to 0 or 1 if you want to import them all",
|
||||
matchedFeaturesMap]).SetClass("flex flex-col")
|
||||
|
||||
super([
|
||||
new Title("Comparison with existing data"),
|
||||
new VariableUiElement(overpassStatus.map(d => {
|
||||
|
@ -205,38 +234,19 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
return new Loading("Querying overpass...")
|
||||
}
|
||||
if(d === "cached"){
|
||||
return new FixedUiElement("Fetched data from local storage")
|
||||
return conflationMaps
|
||||
}
|
||||
if(d === "success"){
|
||||
return new FixedUiElement("Data loaded")
|
||||
return conflationMaps
|
||||
}
|
||||
return new FixedUiElement("Unexpected state "+d).SetClass("alert")
|
||||
})),
|
||||
new VariableUiElement(
|
||||
geojson.map(geojson => {
|
||||
if (geojson === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => {
|
||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
|
||||
mimetype: "application/json+geo"
|
||||
})
|
||||
});
|
||||
})),
|
||||
}))
|
||||
|
||||
new Title("Live data on OSM"),
|
||||
osmLiveData,
|
||||
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"),
|
||||
|
||||
new Title("Nearby features"),
|
||||
new Combine([ "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
|
||||
new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"),
|
||||
"Set the range to 0 or 1 if you want to import them all",
|
||||
matchedFeaturesMap
|
||||
])
|
||||
|
||||
this.IsValid = new UIEventSource(false)
|
||||
this.Value = new UIEventSource(undefined)
|
||||
}
|
||||
this.Value = paritionedImport.map(feats => ({features: feats?.noNearby, layer: params.layer}))
|
||||
this.Value.addCallbackAndRun(v => console.log("ConflationChecker-step value is ", v))
|
||||
this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0)
|
||||
}
|
||||
|
||||
}
|
82
UI/ImportFlow/CreateNotes.ts
Normal file
82
UI/ImportFlow/CreateNotes.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Title from "../Base/Title";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Loading from "../Base/Loading";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Link from "../Base/Link";
|
||||
|
||||
export class CreateNotes extends Combine {
|
||||
|
||||
constructor(state: { osmConnection: OsmConnection }, v: { features: any[]; wikilink: string; intro: string; source: string, theme: string }) {
|
||||
|
||||
const createdNotes: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||
const failed = new UIEventSource<string[]>([])
|
||||
const currentNote = createdNotes.map(n => n.length)
|
||||
|
||||
for (const f of v.features) {
|
||||
|
||||
const src = f.properties["source"] ?? f.properties["src"] ?? v.source
|
||||
delete f.properties["source"]
|
||||
delete f.properties["src"]
|
||||
|
||||
const tags: string [] = []
|
||||
for (const key in f.properties) {
|
||||
if(f.properties[key] === ""){
|
||||
continue
|
||||
}
|
||||
tags.push(key + "=" + f.properties[key].replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
|
||||
}
|
||||
const lat = f.geometry.coordinates[1]
|
||||
const lon = f.geometry.coordinates[0]
|
||||
const text = [v.intro,
|
||||
'',
|
||||
"Source: " + src,
|
||||
'More information at ' + v.wikilink,
|
||||
'',
|
||||
'Import this point easily with',
|
||||
`https://mapcomplete.osm.be/${v.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
|
||||
...tags].join("\n")
|
||||
|
||||
state.osmConnection.openNote(
|
||||
lat, lon, text)
|
||||
.then(({id}) => {
|
||||
createdNotes.data.push(id)
|
||||
createdNotes.ping()
|
||||
}, err => {
|
||||
failed.data.push(err)
|
||||
failed.ping()
|
||||
})
|
||||
}
|
||||
|
||||
super([
|
||||
new Title("Creating notes"),
|
||||
"Hang on while we are importing...",
|
||||
new Toggle(
|
||||
new Loading(new VariableUiElement(currentNote.map(count => new FixedUiElement("Imported <b>" + count + "</b> out of " + v.features.length + " notes")))),
|
||||
new FixedUiElement("All done!"),
|
||||
currentNote.map(count => count < v.features.length)
|
||||
),
|
||||
new VariableUiElement(failed.map(failed => {
|
||||
|
||||
if (failed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine([
|
||||
new FixedUiElement("Some entries failed").SetClass("alert"),
|
||||
...failed
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
})),
|
||||
new VariableUiElement(createdNotes.map(notes => {
|
||||
const links = notes.map(n =>
|
||||
new Link(new FixedUiElement("https://openstreetmap.org/note/" + n), "https://openstreetmap.org/note/" + n, true));
|
||||
return new Combine(links).SetClass("flex flex-col");
|
||||
}))
|
||||
])
|
||||
this.SetClass("flex flex-col");
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,23 @@ import Table from "../Base/Table";
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {FlowStep} from "./FlowStep";
|
||||
import {Layer} from "leaflet";
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import {AllTagsPanel} from "../SpecialVisualizations";
|
||||
import Title from "../Base/Title";
|
||||
|
||||
class PreviewPanel extends ScrollableFullScreen {
|
||||
|
||||
constructor(tags, layer) {
|
||||
super(
|
||||
_ => new FixedUiElement("Element to import"),
|
||||
_ => new Combine(["The tags are:",
|
||||
new AllTagsPanel(tags)
|
||||
]).SetClass("flex flex-col"),
|
||||
"element"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the data to import on a map, asks for the correct layer to be selected
|
||||
|
@ -36,7 +52,6 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
const t = Translations.t.importHelper;
|
||||
|
||||
const propertyKeys = new Set<string>()
|
||||
console.log("Datapanel input got ", geojson)
|
||||
for (const f of geojson.features) {
|
||||
Object.keys(f.properties).forEach(key => propertyKeys.add(key))
|
||||
}
|
||||
|
@ -56,6 +71,7 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
!layer.source.osmTags.matchesProperties(f.properties)
|
||||
)
|
||||
if (!mismatched) {
|
||||
console.log("Autodected layer", layer.id)
|
||||
layerPicker.GetValue().setData(layer);
|
||||
layerPicker.GetValue().addCallback(_ => autodetected.setData(false))
|
||||
autodetected.setData(true)
|
||||
|
@ -96,25 +112,22 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
map.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
new ShowDataMultiLayer({
|
||||
layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers().map(l => ({
|
||||
layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers()
|
||||
.filter(l => l.source.geojsonSource === undefined)
|
||||
.map(l => ({
|
||||
layerDef: l,
|
||||
isDisplayed: new UIEventSource<boolean>(true),
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
|
||||
}))),
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource(matching, false),
|
||||
state: {
|
||||
...state,
|
||||
filteredLayers: new UIEventSource<FilteredLayer[]>(undefined),
|
||||
backgroundLayer: background
|
||||
},
|
||||
leafletMap: map.leafletMap,
|
||||
|
||||
popup: (tag, layer) => new PreviewPanel(tag, layer).SetClass("font-lg")
|
||||
})
|
||||
var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates]))))
|
||||
|
||||
super([
|
||||
"Has " + geojson.features.length + " features",
|
||||
new Title(geojson.features.length + " features to import"),
|
||||
layerPicker,
|
||||
new Toggle("Automatically detected layer", undefined, autodetected),
|
||||
new Table(["", "Key", "Values", "Unique values seen"],
|
||||
|
|
|
@ -8,7 +8,7 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
import Toggle from "../Input/Toggle";
|
||||
import {UIElement} from "../UIElement";
|
||||
|
||||
export interface FlowStep<T> extends BaseUIElement{
|
||||
export interface FlowStep<T> extends BaseUIElement {
|
||||
readonly IsValid: UIEventSource<boolean>
|
||||
readonly Value: UIEventSource<T>
|
||||
}
|
||||
|
@ -16,70 +16,97 @@ export interface FlowStep<T> extends BaseUIElement{
|
|||
export class FlowPanelFactory<T> {
|
||||
private _initial: FlowStep<any>;
|
||||
private _steps: ((x: any) => FlowStep<any>)[];
|
||||
private _stepNames: string[];
|
||||
|
||||
private constructor(initial: FlowStep<any>, steps: ((x:any) => FlowStep<any>)[], stepNames: string[]) {
|
||||
private _stepNames: (string | BaseUIElement)[];
|
||||
|
||||
private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) {
|
||||
this._initial = initial;
|
||||
this._steps = steps;
|
||||
this._stepNames = stepNames;
|
||||
}
|
||||
|
||||
public static start<TOut> (step: FlowStep<TOut>): FlowPanelFactory<TOut>{
|
||||
return new FlowPanelFactory(step, [], [])
|
||||
|
||||
public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
|
||||
return new FlowPanelFactory(step, [], [name])
|
||||
}
|
||||
|
||||
public then<TOut>(name: string, construct: ((t:T) => FlowStep<TOut>)): FlowPanelFactory<TOut>{
|
||||
|
||||
public then<TOut>(name: string | BaseUIElement, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> {
|
||||
return new FlowPanelFactory<TOut>(
|
||||
this._initial,
|
||||
this._steps.concat([construct]),
|
||||
this._stepNames.concat([name])
|
||||
)
|
||||
}
|
||||
|
||||
public finish(construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)) : BaseUIElement {
|
||||
|
||||
public finish(name: string | BaseUIElement, construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)): {
|
||||
flow: BaseUIElement,
|
||||
furthestStep: UIEventSource<number>,
|
||||
titles: (string | BaseUIElement)[]
|
||||
} {
|
||||
const furthestStep = new UIEventSource(0)
|
||||
// Construct all the flowpanels step by step (in reverse order)
|
||||
const nextConstr : ((t:any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined)
|
||||
const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined)
|
||||
nextConstr.push(construct)
|
||||
|
||||
for (let i = this._steps.length - 1; i >= 0; i--){
|
||||
const createFlowStep : (value) => FlowStep<any> = this._steps[i];
|
||||
for (let i = this._steps.length - 1; i >= 0; i--) {
|
||||
const createFlowStep: (value) => FlowStep<any> = this._steps[i];
|
||||
const isConfirm = i == this._steps.length - 1;
|
||||
nextConstr[i] = (value, backButton) => {
|
||||
console.log("Creating flowSTep ", this._stepNames[i])
|
||||
const flowStep = createFlowStep(value)
|
||||
return new FlowPanel(flowStep, nextConstr[i + 1], backButton);
|
||||
furthestStep.setData(i + 1);
|
||||
const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm);
|
||||
panel.isActive.addCallbackAndRun(active => {
|
||||
if (active) {
|
||||
furthestStep.setData(i + 1);
|
||||
}
|
||||
})
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
return new FlowPanel(this._initial, nextConstr[0],undefined)
|
||||
|
||||
const flow = new FlowPanel(this._initial, nextConstr[0])
|
||||
flow.isActive.addCallbackAndRun(active => {
|
||||
if (active) {
|
||||
furthestStep.setData(0);
|
||||
}
|
||||
})
|
||||
return {
|
||||
flow,
|
||||
furthestStep,
|
||||
titles: this._stepNames
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class FlowPanel<T> extends Toggle {
|
||||
|
||||
public isActive: UIEventSource<boolean>
|
||||
|
||||
constructor(
|
||||
initial: (FlowStep<T>),
|
||||
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
|
||||
backbutton?: BaseUIElement
|
||||
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
|
||||
backbutton?: BaseUIElement,
|
||||
isConfirm = false
|
||||
) {
|
||||
const t = Translations.t.general;
|
||||
|
||||
|
||||
const currentStepActive = new UIEventSource(true);
|
||||
|
||||
let nextStep: UIEventSource<BaseUIElement>= new UIEventSource<BaseUIElement>(undefined)
|
||||
let nextStep: UIEventSource<BaseUIElement> = new UIEventSource<BaseUIElement>(undefined)
|
||||
const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => {
|
||||
currentStepActive.setData(true)
|
||||
})
|
||||
|
||||
let elements : (BaseUIElement | string)[] = []
|
||||
if(initial !== undefined){
|
||||
|
||||
let elements: (BaseUIElement | string)[] = []
|
||||
if (initial !== undefined) {
|
||||
// Startup the flow
|
||||
elements = [
|
||||
initial,
|
||||
new Combine([
|
||||
backbutton,
|
||||
new Toggle(
|
||||
new SubtleButton(Svg.back_svg().SetStyle("transform: rotate(180deg);"), t.next).onClick(() => {
|
||||
new SubtleButton(
|
||||
isConfirm ? Svg.checkmark_svg() :
|
||||
Svg.back_svg().SetStyle("transform: rotate(180deg);"),
|
||||
isConfirm ? t.confirm : t.next
|
||||
).onClick(() => {
|
||||
const v = initial.Value.data;
|
||||
nextStep.setData(constructNextstep(v, backButtonForNextStep))
|
||||
currentStepActive.setData(false)
|
||||
|
@ -88,18 +115,18 @@ export class FlowPanel<T> extends Toggle {
|
|||
initial.IsValid
|
||||
)
|
||||
]).SetClass("flex w-full justify-end space-x-2")
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
super(
|
||||
new Combine(elements).SetClass("h-full flex flex-col justify-between"),
|
||||
new VariableUiElement(nextStep),
|
||||
currentStepActive
|
||||
);
|
||||
this.isActive = currentStepActive
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -9,11 +9,18 @@ import MoreScreen from "../BigComponents/MoreScreen";
|
|||
import MinimapImplementation from "../Base/MinimapImplementation";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
import {FlowPanel, FlowPanelFactory} from "./FlowStep";
|
||||
import {FlowPanelFactory} from "./FlowStep";
|
||||
import {RequestFile} from "./RequestFile";
|
||||
import {DataPanel} from "./DataPanel";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import ConflationChecker from "./ConflationChecker";
|
||||
import {AskMetadata} from "./AskMetadata";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {ConfirmProcess} from "./ConfirmProcess";
|
||||
import {CreateNotes} from "./CreateNotes";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import List from "../Base/List";
|
||||
import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes";
|
||||
|
||||
export default class ImportHelperGui extends LoginToggle {
|
||||
constructor() {
|
||||
|
@ -24,28 +31,51 @@ export default class ImportHelperGui extends LoginToggle {
|
|||
// We disable the userbadge, as various 'showData'-layers will give a read-only view in this case
|
||||
state.featureSwitchUserbadge.setData(false)
|
||||
|
||||
const {flow, furthestStep, titles} =
|
||||
FlowPanelFactory
|
||||
.start("Select file", new RequestFile())
|
||||
.then("Inspect data", geojson => new DataPanel(state, geojson))
|
||||
.then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v))
|
||||
.then("Compare with existing data", v => new ConflationChecker(state, v))
|
||||
.then("License and community check", v => new ConfirmProcess(v))
|
||||
.then("Metadata", (v:{features:any[], layer: LayerConfig}) => new AskMetadata(v))
|
||||
.finish("Note creation", v => new CreateNotes(state, v));
|
||||
|
||||
const toc = new List(
|
||||
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
|
||||
if(i > currentStep){
|
||||
return new Combine([title]).SetClass("subtle");
|
||||
}
|
||||
if(i == currentStep){
|
||||
return new Combine([title]).SetClass("font-bold");
|
||||
}
|
||||
if(i < currentStep){
|
||||
return title
|
||||
}
|
||||
|
||||
|
||||
})))
|
||||
, true)
|
||||
|
||||
const leftContents: BaseUIElement[] = [
|
||||
new BackToIndex().SetClass("block pl-4"),
|
||||
toc,
|
||||
new Toggle(new FixedUiElement("Testmode - won't actually import notes").SetClass("alert"), undefined, state.featureSwitchIsTesting),
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
|
||||
].map(el => el?.SetClass("pl-4"))
|
||||
|
||||
const leftBar = new Combine([
|
||||
new Combine(leftContents).SetClass("sticky top-4 m-4")
|
||||
]).SetClass("block w-full md:w-2/6 lg:w-1/6")
|
||||
new Combine(leftContents).SetClass("sticky top-4 m-4"),
|
||||
]).SetClass("block w-full md:w-2/6 lg:w-1/6")
|
||||
|
||||
|
||||
|
||||
|
||||
const mainPanel =
|
||||
FlowPanelFactory
|
||||
.start(new RequestFile())
|
||||
.then("datapanel", geojson => new DataPanel(state, geojson))
|
||||
.then("conflation", v => new ConflationChecker(state, v))
|
||||
.finish(_ => new FixedUiElement("All done!"))
|
||||
|
||||
super(
|
||||
new Toggle(
|
||||
new Combine([
|
||||
leftBar,
|
||||
mainPanel.SetClass("m-8 w-full mb-24")
|
||||
flow.SetClass("m-8 w-full mb-24")
|
||||
]).SetClass("h-full block md:flex")
|
||||
|
||||
,
|
||||
|
|
28
UI/ImportFlow/ImportUtils.ts
Normal file
28
UI/ImportFlow/ImportUtils.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
|
||||
export class ImportUtils {
|
||||
public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: UIEventSource<{ features: any[] }>, cutoffDistanceInMeters: UIEventSource<number>): UIEventSource<{ hasNearby: any[], noNearby: any[] }> {
|
||||
return compareWith.map(osmData => {
|
||||
if (osmData?.features === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const maxDist = cutoffDistanceInMeters.data
|
||||
|
||||
|
||||
const hasNearby = []
|
||||
const noNearby = []
|
||||
for (const toImportElement of toPartitionFeatureCollection.features) {
|
||||
const hasNearbyFeature = osmData.features.some(f =>
|
||||
maxDist >= GeoOperations.distanceBetween(toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))
|
||||
if (hasNearbyFeature) {
|
||||
hasNearby.push(toImportElement)
|
||||
} else {
|
||||
noNearby.push(toImportElement)
|
||||
}
|
||||
}
|
||||
|
||||
return {hasNearby, noNearby}
|
||||
}, [cutoffDistanceInMeters]);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue