Finish importer, add applicable import layers to every theme by default

This commit is contained in:
Pieter Vander Vennet 2022-01-21 01:57:16 +01:00
parent 3402ac0954
commit ca1490902c
41 changed files with 1559 additions and 898 deletions

View 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;
})
}
}

View 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])
}
}

View 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)
}
}

View file

@ -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)
}
}

View 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");
}
}

View file

@ -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"],

View file

@ -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
}
}

View file

@ -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")
,

View 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]);
}
}