forked from MapComplete/MapComplete
Add more checks in the import helper after user testing
This commit is contained in:
parent
2dac893bb3
commit
9617dbc34d
15 changed files with 344 additions and 94 deletions
|
@ -9,6 +9,14 @@ import {DropDown} from "../Input/DropDown";
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {RadioButton} from "../Input/RadioButton";
|
||||
import {FixedInputElement} from "../Input/FixedInputElement";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import Img from "../Base/Img";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import Toggleable from "../Base/Toggleable";
|
||||
|
||||
export class AskMetadata extends Combine implements FlowStep<{
|
||||
features: any[],
|
||||
|
@ -27,14 +35,14 @@ export class AskMetadata extends Combine implements FlowStep<{
|
|||
}>;
|
||||
public readonly IsValid: UIEventSource<boolean>;
|
||||
|
||||
constructor(params: ({ features: any[], layer: LayerConfig })) {
|
||||
constructor(params: ({ features: any[], theme: string })) {
|
||||
|
||||
const introduction = ValidatedTextField.ForType("text").ConstructInputElement({
|
||||
value: LocalStorageSource.Get("import-helper-introduction-text"),
|
||||
inputStyle: "width: 100%"
|
||||
})
|
||||
|
||||
const wikilink = ValidatedTextField.ForType("string").ConstructInputElement({
|
||||
const wikilink = ValidatedTextField.ForType("url").ConstructInputElement({
|
||||
value: LocalStorageSource.Get("import-helper-wikilink-text"),
|
||||
inputStyle: "width: 100%"
|
||||
})
|
||||
|
@ -44,26 +52,6 @@ export class AskMetadata extends Combine implements FlowStep<{
|
|||
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.ForType("string").ConstructInputElement({
|
||||
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.",
|
||||
|
@ -73,7 +61,20 @@ export class AskMetadata extends Combine implements FlowStep<{
|
|||
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
|
||||
new VariableUiElement(wikilink.GetValue().map(wikilink => {
|
||||
try{
|
||||
const url = new URL(wikilink)
|
||||
if(url.hostname.toLowerCase() !== "wiki.openstreetmap.org"){
|
||||
return new FixedUiElement("Expected a link to wiki.openstreetmap.org").SetClass("alert");
|
||||
}
|
||||
|
||||
if(url.pathname.toLowerCase() === "/wiki/main_page"){
|
||||
return new FixedUiElement("Nope, the home page isn't allowed either. Enter the URL of a proper wikipage documenting your import").SetClass("alert");
|
||||
}
|
||||
}catch(e){
|
||||
return new FixedUiElement("Not a valid URL").SetClass("alert")
|
||||
}
|
||||
}))
|
||||
]);
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
|
@ -83,16 +84,30 @@ export class AskMetadata extends Combine implements FlowStep<{
|
|||
wikilink: wikilink.GetValue().data,
|
||||
intro,
|
||||
source: source.GetValue().data,
|
||||
theme: theme.GetValue().data
|
||||
|
||||
theme: params.theme
|
||||
}
|
||||
}, [wikilink.GetValue(), source.GetValue(), theme.GetValue()])
|
||||
}, [wikilink.GetValue(), source.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;
|
||||
if ([ obj.features, obj.intro, obj.wikilink, obj.source].some(v => v === undefined)){
|
||||
console.log("Obj is", obj)
|
||||
return false;
|
||||
}
|
||||
|
||||
try{
|
||||
const url = new URL(obj.wikilink)
|
||||
if(url.hostname.toLowerCase() !== "wiki.openstreetmap.org"){
|
||||
return false;
|
||||
}
|
||||
}catch(e){
|
||||
return false
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ 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";
|
||||
|
@ -17,7 +16,6 @@ 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";
|
||||
|
@ -27,19 +25,19 @@ 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 }> {
|
||||
export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> {
|
||||
|
||||
public IsValid: UIEventSource<boolean>
|
||||
public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, geojson: any }>
|
||||
public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[] , theme: string}>
|
||||
|
||||
|
||||
constructor(state, params: { bbox: BBox, layer: LayerConfig, geojson: { features: any[] } }) {
|
||||
constructor(state, params: { bbox: BBox, layer: LayerConfig, features: any[], theme: string }) {
|
||||
|
||||
const layerConfig = known_layers.layers.filter(l => l.id === params.layer.id)[0]
|
||||
if (layerConfig === undefined) {
|
||||
console.error("WEIRD: layer not found in the builtin layer overview")
|
||||
}
|
||||
const importLayerJson = new CreateNoteImportLayer(365).convertStrict(<LayerConfigJson>layerConfig, "CompareToAlreadyExistingNotes")
|
||||
const importLayerJson = new CreateNoteImportLayer(150).convertStrict(<LayerConfigJson>layerConfig, "CompareToAlreadyExistingNotes")
|
||||
const importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic")
|
||||
const flayer: FilteredLayer = {
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()),
|
||||
|
@ -47,12 +45,13 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
|||
layerDef: importLayer
|
||||
}
|
||||
const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
|
||||
|
||||
allNotesWithinBbox.features.map(f => MetaTagging.addMetatags(
|
||||
f,
|
||||
{
|
||||
memberships: new RelationsTracker(),
|
||||
getFeaturesWithin: (layerId, bbox: BBox) => [],
|
||||
getFeatureById: (id: string) => undefined
|
||||
getFeaturesWithin: () => [],
|
||||
getFeatureById: () => undefined
|
||||
},
|
||||
importLayer,
|
||||
state,
|
||||
|
@ -84,9 +83,9 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
|||
})
|
||||
|
||||
|
||||
const maxDistance = new UIEventSource<number>(5)
|
||||
const maxDistance = new UIEventSource<number>(10)
|
||||
|
||||
const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(params.geojson, alreadyOpenImportNotes.features
|
||||
const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(params, alreadyOpenImportNotes.features
|
||||
.map(ff => ({features: ff.map(ff => ff.feature)})), maxDistance)
|
||||
|
||||
|
||||
|
@ -103,34 +102,53 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
|||
new Title("Compare with already existing 'to-import'-notes"),
|
||||
new VariableUiElement(
|
||||
alreadyOpenImportNotes.features.map(notesWithImport => {
|
||||
if(allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined){
|
||||
return new FixedUiElement("Loading notes failed: "+allNotesWithinBbox.state.data["error"] )
|
||||
}
|
||||
if (allNotesWithinBbox.features.data === undefined || allNotesWithinBbox.features.data.length === 0) {
|
||||
return new Loading("Fetching notes from OSM")
|
||||
}
|
||||
if (notesWithImport.length === 0) {
|
||||
return new FixedUiElement("No previous note to import found").SetClass("thanks")
|
||||
return new FixedUiElement("No previous import notes found").SetClass("thanks")
|
||||
}
|
||||
return new Combine([
|
||||
"The red elements on the next map are all the data points from your dataset. There are <b>"+params.features.length+"</b> elements in your dataset.",
|
||||
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 VariableUiElement( partitionedImportPoints.map(({noNearby, hasNearby}) => {
|
||||
|
||||
if(noNearby.length === 0){
|
||||
// Nothing can be imported
|
||||
return new FixedUiElement("All of the proposed points have (or had) an import note already").SetClass("alert w-full block").SetStyle("padding: 0.5rem")
|
||||
}
|
||||
|
||||
if(hasNearby.length === 0){
|
||||
// All points can be imported
|
||||
return new FixedUiElement("All of the proposed points have don't have a previous import note nearby").SetClass("thanks w-full block").SetStyle("padding: 0.5rem")
|
||||
|
||||
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"),
|
||||
comparisonMap,
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
new FixedUiElement(hasNearby.length+" points do have an already existing import note within "+maxDistance.data+" meter.").SetClass("alert"),
|
||||
"These data points will <i>not</i> be imported and are shown as red dots on the map below",
|
||||
comparisonMap.SetClass("w-full")
|
||||
]).SetClass("w-full")
|
||||
}))
|
||||
|
||||
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
}, [allNotesWithinBbox.features])
|
||||
}, [allNotesWithinBbox.features, allNotesWithinBbox.state])
|
||||
),
|
||||
|
||||
|
||||
]);
|
||||
this.SetClass("flex flex-col")
|
||||
this.Value = partitionedImportPoints.map(({noNearby}) => ({
|
||||
geojson: {features: noNearby, type: "FeatureCollection"},
|
||||
features: noNearby,
|
||||
bbox: params.bbox,
|
||||
layer: params.layer
|
||||
layer: params.layer,
|
||||
theme: params.theme
|
||||
}))
|
||||
|
||||
this.IsValid = alreadyOpenImportNotes.features.map(ff => {
|
||||
|
|
|
@ -8,14 +8,13 @@ import Title from "../Base/Title";
|
|||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import {Utils} from "../../Utils";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export class ConfirmProcess extends Combine implements FlowStep<{ features: any[], layer: LayerConfig }> {
|
||||
export class ConfirmProcess extends Combine implements FlowStep<{ features: any[], theme: string }> {
|
||||
|
||||
public IsValid: UIEventSource<boolean>
|
||||
public Value: UIEventSource<{ features: any[], layer: LayerConfig }>
|
||||
public Value: UIEventSource<{ features: any[],theme: string }>
|
||||
|
||||
constructor(v: { features: any[], layer: LayerConfig }) {
|
||||
constructor(v: { features: any[], theme: string }) {
|
||||
|
||||
const toConfirm = [
|
||||
new Combine(["I have read the ", new Link("import guidelines on the OSM wiki", "https://wiki.openstreetmap.org/wiki/Import_guidelines", true)]),
|
||||
|
@ -35,13 +34,13 @@ export class ConfirmProcess extends Combine implements FlowStep<{ features: any[
|
|||
type:"FeatureCollection",
|
||||
features: v.features
|
||||
}
|
||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "prepared_import_"+v.layer.id+".geojson",{
|
||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "prepared_import_"+v.theme+".geojson",{
|
||||
mimetype: "application/vnd.geo+json"
|
||||
})
|
||||
})
|
||||
]);
|
||||
this.SetClass("link-underline")
|
||||
this.IsValid = licenseClear.GetValue().map(selected => toConfirm.length == selected.length)
|
||||
this.Value = new UIEventSource<{ features: any[], layer: LayerConfig }>(v)
|
||||
this.Value = new UIEventSource<{ features: any[], theme: string }>(v)
|
||||
}
|
||||
}
|
|
@ -32,24 +32,25 @@ 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<{ features: any[], layer: LayerConfig }> {
|
||||
export default class ConflationChecker extends Combine implements FlowStep<{ features: any[], theme: string }> {
|
||||
|
||||
public readonly IsValid
|
||||
public readonly Value
|
||||
|
||||
constructor(
|
||||
state,
|
||||
params: { bbox: BBox, layer: LayerConfig, geojson: any }) {
|
||||
params: { bbox: BBox, layer: LayerConfig, theme: string, features: any[] }) {
|
||||
|
||||
|
||||
const bbox = params.bbox.padAbsolute(0.0001)
|
||||
const layer = params.layer;
|
||||
const toImport = params.geojson;
|
||||
const toImport: {features: any[]} = params;
|
||||
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) {
|
||||
if (v !== undefined && v !== null) {
|
||||
console.log("Loaded from local storage:", v)
|
||||
const [geojson, date] = v;
|
||||
const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
|
||||
|
@ -213,12 +214,15 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
})),
|
||||
|
||||
new Title("Live data on OSM"),
|
||||
"The "+toImport.features.length+" red elements on the following map are all your import candidates.",
|
||||
new VariableUiElement(geojson.map(geojson => new FixedUiElement((geojson?.features?.length ?? "No") + " elements are loaded from OpenStreetMap which match the layer "+layer.id+". Zooming in might be needed to show them"))),
|
||||
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"),
|
||||
new VariableUiElement(toImportWithNearby.features.map(feats =>
|
||||
new FixedUiElement("The "+ feats.length +" 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")
|
||||
|
||||
|
@ -246,7 +250,6 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
])
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import LoginToImport from "./LoginToImport";
|
|||
import {MapPreview} from "./MapPreview";
|
||||
import LeftIndex from "../Base/LeftIndex";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import SelectTheme from "./SelectTheme";
|
||||
|
||||
export default class ImportHelperGui extends LeftIndex {
|
||||
constructor() {
|
||||
|
@ -31,14 +32,15 @@ export default class ImportHelperGui extends LeftIndex {
|
|||
FlowPanelFactory
|
||||
.start("Introduction", new Introdution())
|
||||
.then("Login", _ => new LoginToImport(state))
|
||||
.then("Select file", _ => new RequestFile())
|
||||
.then("Inspect attributes", geojson => new PreviewPanel(state, geojson))
|
||||
.then("Inspect data", geojson => new MapPreview(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));
|
||||
.then("Select file", _ => new RequestFile())
|
||||
.then("Inspect attributes", geojson => new PreviewPanel(state, geojson))
|
||||
.then("Inspect data", geojson => new MapPreview(state, geojson))
|
||||
.then("Select theme", v => new SelectTheme(v))
|
||||
.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 : {features: any[], theme: string}) => new ConfirmProcess(v))
|
||||
.then("Metadata", (v: { features: any[], layer: LayerConfig, theme: string }) => new AskMetadata(v))
|
||||
.finish("Note creation", v => new CreateNotes(state, v));
|
||||
|
||||
const toc = new List(
|
||||
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import Toggle from "../Input/Toggle";
|
|||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import MoreScreen from "../BigComponents/MoreScreen";
|
||||
import CheckBoxes from "../Input/Checkboxes";
|
||||
|
||||
export default class LoginToImport extends Combine implements FlowStep<UserRelatedState> {
|
||||
readonly IsValid: UIEventSource<boolean>;
|
||||
|
@ -21,7 +22,9 @@ export default class LoginToImport extends Combine implements FlowStep<UserRelat
|
|||
|
||||
constructor(state: UserRelatedState) {
|
||||
const t = Translations.t.importHelper
|
||||
const isValid = state.osmConnection.userDetails.map(ud => LoginToImport.whitelist.indexOf(ud.uid) >= 0 || ud.csCount >= Constants.userJourney.importHelperUnlock)
|
||||
const check = new CheckBoxes([new VariableUiElement(state.osmConnection.userDetails.map(ud => t.loginIsCorrect.Subs(ud)))])
|
||||
const isValid = state.osmConnection.userDetails.map(ud =>
|
||||
LoginToImport.whitelist.indexOf(ud.uid) >= 0 || ud.csCount >= Constants.userJourney.importHelperUnlock)
|
||||
super([
|
||||
new Title(t.userAccountTitle),
|
||||
new LoginToggle(
|
||||
|
@ -33,7 +36,8 @@ export default class LoginToImport extends Combine implements FlowStep<UserRelat
|
|||
new Img(ud.img ?? "./assets/svgs/help.svg").SetClass("w-16 h-16 rounded-full"),
|
||||
t.loggedInWith.Subs(ud),
|
||||
new SubtleButton(Svg.logout_svg().SetClass("h-8"), Translations.t.general.logout)
|
||||
.onClick(() => state.osmConnection.LogOut())
|
||||
.onClick(() => state.osmConnection.LogOut()),
|
||||
check
|
||||
]);
|
||||
})),
|
||||
t.loginRequired,
|
||||
|
@ -46,6 +50,6 @@ export default class LoginToImport extends Combine implements FlowStep<UserRelat
|
|||
, isValid)
|
||||
])
|
||||
this.Value = new UIEventSource<UserRelatedState>(state)
|
||||
this.IsValid = isValid;
|
||||
this.IsValid = isValid.map(isValid => isValid && check.GetValue().data.length > 0, [check.GetValue()]);
|
||||
}
|
||||
}
|
|
@ -42,9 +42,9 @@ class PreviewPanel extends ScrollableFullScreen {
|
|||
/**
|
||||
* Shows the data to import on a map, asks for the correct layer to be selected
|
||||
*/
|
||||
export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, geojson: any }> {
|
||||
export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[] }> {
|
||||
public readonly IsValid: UIEventSource<boolean>;
|
||||
public readonly Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, geojson: any }>
|
||||
public readonly Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[] }>
|
||||
|
||||
constructor(
|
||||
state: UserRelatedState,
|
||||
|
@ -153,7 +153,7 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
this.Value = bbox.map(bbox =>
|
||||
({
|
||||
bbox,
|
||||
geojson,
|
||||
features: geojson.features,
|
||||
layer: layerPicker.GetValue().data
|
||||
}), [layerPicker.GetValue()])
|
||||
|
||||
|
|
|
@ -34,13 +34,13 @@ class FileSelector extends InputElementMap<FileList, { name: string, contents: P
|
|||
/**
|
||||
* The first step in the import flow: load a file and validate that it is a correct geojson or CSV file
|
||||
*/
|
||||
export class RequestFile extends Combine implements FlowStep<any> {
|
||||
export class RequestFile extends Combine implements FlowStep<{features: any[]}> {
|
||||
|
||||
public readonly IsValid: UIEventSource<boolean>
|
||||
/**
|
||||
* The loaded GeoJSON
|
||||
*/
|
||||
public readonly Value: UIEventSource<any>
|
||||
public readonly Value: UIEventSource<{features: any[]}>
|
||||
|
||||
constructor() {
|
||||
const t = Translations.t.importHelper.selectFile;
|
||||
|
|
125
UI/ImportFlow/SelectTheme.ts
Normal file
125
UI/ImportFlow/SelectTheme.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import {FlowStep} from "./FlowStep";
|
||||
import Combine from "../Base/Combine";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
import {FixedInputElement} from "../Input/FixedInputElement";
|
||||
import Img from "../Base/Img";
|
||||
import Title from "../Base/Title";
|
||||
import {RadioButton} from "../Input/RadioButton";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Toggleable from "../Base/Toggleable";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
|
||||
export default class SelectTheme extends Combine implements FlowStep<{
|
||||
features: any[],
|
||||
theme: string,
|
||||
layer: LayerConfig,
|
||||
bbox: BBox,
|
||||
}> {
|
||||
|
||||
public readonly Value: UIEventSource<{
|
||||
features: any[],
|
||||
theme: string,
|
||||
layer: LayerConfig,
|
||||
bbox: BBox,
|
||||
}>;
|
||||
public readonly IsValid: UIEventSource<boolean>;
|
||||
|
||||
constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) {
|
||||
|
||||
let options: InputElement<string>[] = AllKnownLayouts.layoutsList
|
||||
.filter(th => th.layers.some(l => l.id === params.layer.id))
|
||||
.filter(th => th.id !== "personal")
|
||||
.map(th => new FixedInputElement<string>(
|
||||
new Combine([
|
||||
new Img(th.icon).SetClass("block h-12 w-12 br-4"),
|
||||
new Title( th.title)
|
||||
]).SetClass("flex items-center"),
|
||||
th.id))
|
||||
|
||||
|
||||
const themeRadios = new RadioButton<string>(options, {
|
||||
selectFirstAsDefault: false
|
||||
})
|
||||
|
||||
|
||||
|
||||
const applicablePresets = themeRadios.GetValue().map(theme => {
|
||||
if(theme === undefined){
|
||||
return []
|
||||
}
|
||||
// we get the layer with the correct ID via the actual theme config, as the actual theme might have different presets due to overrides
|
||||
const themeConfig = AllKnownLayouts.layoutsList.find(th => th.id === theme)
|
||||
const layer = themeConfig.layers.find(l => l.id === params.layer.id)
|
||||
return layer.presets
|
||||
})
|
||||
|
||||
|
||||
const nonMatchedElements = applicablePresets.map(presets => {
|
||||
if(presets === undefined || presets.length === 0){
|
||||
return undefined
|
||||
}
|
||||
return params.features.filter(feat => !presets.some(preset => new And(preset.tags).matchesProperties(feat.properties)))
|
||||
})
|
||||
|
||||
super([
|
||||
new Title("Select a theme"),
|
||||
"All of the following themes will show the import notes. However, the note on OpenStreetMap can link to only one single theme. Choose which theme that the created notes will link to",
|
||||
themeRadios,
|
||||
new VariableUiElement(applicablePresets.map(applicablePresets => {
|
||||
if(themeRadios.GetValue().data === undefined){
|
||||
return undefined
|
||||
}
|
||||
if(applicablePresets === undefined || applicablePresets.length === 0){
|
||||
return new FixedUiElement("This theme has no presets loaded. As a result, imports won't work here").SetClass("alert")
|
||||
}
|
||||
},[themeRadios.GetValue()])),
|
||||
new VariableUiElement(nonMatchedElements.map(unmatched => {
|
||||
if(unmatched === undefined || unmatched.length === 0){
|
||||
return
|
||||
}
|
||||
return new Combine([new FixedUiElement(unmatched.length+" objects dont match any presets").SetClass("alert"),
|
||||
...applicablePresets.data.map(preset => preset.title.txt +" needs tags "+ preset.tags.map(t => t.asHumanString()).join(" & ")),
|
||||
,
|
||||
new Toggleable( new Title( "The following elements don't match any of the presets"),
|
||||
new Combine( unmatched.map(feat => JSON.stringify(feat.properties))).SetClass("flex flex-col")
|
||||
)
|
||||
|
||||
]) .SetClass("flex flex-col")
|
||||
|
||||
}))
|
||||
]);
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
this.Value = themeRadios.GetValue().map(theme => ({
|
||||
features: params.features,
|
||||
layer: params.layer,
|
||||
bbox: params.bbox,
|
||||
theme
|
||||
}))
|
||||
|
||||
this.IsValid = this.Value.map(obj => {
|
||||
if (obj === undefined) {
|
||||
return false;
|
||||
}
|
||||
if ([obj.theme, obj.features].some(v => v === undefined)){
|
||||
return false;
|
||||
}
|
||||
if(applicablePresets.data === undefined || applicablePresets.data.length === 0){
|
||||
return false
|
||||
}
|
||||
if((nonMatchedElements.data?.length??0) > 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}, [applicablePresets])
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue