Add more checks in the import helper after user testing

This commit is contained in:
pietervdvn 2022-03-24 03:11:29 +01:00
parent 2dac893bb3
commit 9617dbc34d
15 changed files with 344 additions and 94 deletions

View file

@ -13,6 +13,7 @@ import {GeoOperations} from "../../GeoOperations";
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly state = new UIEventSource<undefined | {error: string} | "loaded">(undefined)
public readonly name;
public readonly isOsmCache: boolean
public readonly layer: FilteredLayer;
@ -80,6 +81,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
const self = this;
Utils.downloadJson(url)
.then(json => {
self.state.setData("loaded")
if (json.features === undefined || json.features === null) {
return;
}
@ -135,7 +137,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
eventSource.setData(eventSource.data.concat(newFeatures))
}).catch(msg => console.debug("Could not load geojson layer", url, "due to", msg))
}).catch(msg => {
console.debug("Could not load geojson layer", url, "due to", msg);
self.state.setData({error: msg})
})
}
}

View file

@ -1,7 +1,6 @@
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc";
import DynamicTileSource from "./DynamicTileSource";
import {Utils} from "../../../Utils";
import GeoJsonSource from "../Sources/GeoJsonSource";
@ -14,7 +13,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
constructor(layer: FilteredLayer,
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
state: {
locationControl: UIEventSource<Loc>
locationControl?: UIEventSource<{zoom?: number}>
currentBounds: UIEventSource<BBox>
}) {
const source = layer.layerDef.source

View file

@ -1,7 +1,6 @@
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc";
import TileHierarchy from "./TileHierarchy";
import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox";
@ -19,30 +18,29 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
state: {
currentBounds: UIEventSource<BBox>;
locationControl: UIEventSource<Loc>
locationControl?: UIEventSource<{zoom?: number}>
}
) {
const self = this;
this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>()
const neededTiles = state.locationControl.map(
location => {
const neededTiles = state.currentBounds.map(
bounds => {
if (bounds === undefined) {
// We'll retry later
return undefined
}
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
// No need to download! - the layer is disabled
return undefined;
}
if (location.zoom < layer.layerDef.minzoom) {
if (state.locationControl?.data?.zoom !== undefined && state.locationControl.data.zoom < layer.layerDef.minzoom) {
// No need to download! - the layer is disabled
return undefined;
}
// Yup, this is cheating to just get the bounds here
const bounds = state.currentBounds.data
if (bounds === undefined) {
// We'll retry later
return undefined
}
const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
if (tileRange.total > 10000) {
console.error("Got a really big tilerange, bounds and location might be out of sync")
@ -55,7 +53,7 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
}
return needed
}
, [layer.isDisplayed, state.currentBounds]).stabilized(250);
, [layer.isDisplayed, state.locationControl]).stabilized(250);
neededTiles.addCallbackAndRunD(neededIndexes => {
console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes)

View file

@ -8,18 +8,24 @@ import {Utils} from "../../Utils";
export class IdbLocalStorage {
public static Get<T>(key: string, options?: { defaultValue?: T, whenLoaded?: (t: T) => void }): UIEventSource<T> {
public static Get<T>(key: string, options?: { defaultValue?: T, whenLoaded?: (t: T | null) => void }): UIEventSource<T> {
const src = new UIEventSource<T>(options?.defaultValue, "idb-local-storage:" + key)
if (Utils.runningFromConsole) {
return src;
}
src.addCallback(v => idb.set(key, v))
idb.get(key).then(v => {
src.setData(v ?? options?.defaultValue);
if (options?.whenLoaded !== undefined) {
options?.whenLoaded(v)
}
}).catch(err => {
console.warn("Loading from local storage failed due to", err)
if (options?.whenLoaded !== undefined) {
options?.whenLoaded(null)
}
})
src.addCallback(v => idb.set(key, v))
return src;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 => {

View file

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

View file

@ -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()])

View file

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

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

View file

@ -280,7 +280,8 @@
"inspectLooksCorrect": "These values look correct",
"lockNotice": "This page is locked. You need {importHelperUnlock} changesets before you can access here.",
"locked": "You need at least {importHelperUnlock} to use the import helper",
"loggedInWith": "You are currently logged in as {name} and have made {csCount} changesets",
"loggedInWith": "You are currently logged in as <b>{name}</b> and have made {csCount} changesets",
"loginIsCorrect": "<b>{name}</b> is the correct account to create the import notes with.",
"loginRequired": "You have to be logged in to continue",
"mapPreview": {
"autodetected": "The layer was automatically deducted based on the properties",

View file

@ -0,0 +1,75 @@
import {describe} from 'mocha'
import {expect} from 'chai'
import {LayoutConfigJson} from "../../../../Models/ThemeConfig/Json/LayoutConfigJson";
import {LayerConfigJson} from "../../../../Models/ThemeConfig/Json/LayerConfigJson";
import {PrepareTheme} from "../../../../Models/ThemeConfig/Conversion/PrepareTheme";
import {TagRenderingConfigJson} from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson";
import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig";
import * as bookcaseLayer from "../../../../assets/generated/layers/public_bookcase.json"
import LayerConfig from "../../../../Models/ThemeConfig/LayerConfig";
import {ExtractImages} from "../../../../Models/ThemeConfig/Conversion/FixImages";
import * as cyclofix from "../../../../assets/generated/themes/cyclofix.json"
const themeConfigJson: LayoutConfigJson = {
description: "Descr",
icon: "",
layers: [
{
builtin: "public_bookcase",
override: {
source: {
geoJson: "xyz"
}
}
}
],
maintainer: "",
startLat: 0,
startLon: 0,
startZoom: 0,
title: {
en: "Title"
},
version: "",
id: "test"
}
describe("PrepareTheme", () => {
it("should apply overrideAll", () => {
const sharedLayers = new Map<string, LayerConfigJson>()
sharedLayers.set("public_bookcase", bookcaseLayer["default"])
let themeConfigJsonPrepared = new PrepareTheme({
tagRenderings: new Map<string, TagRenderingConfigJson>(),
sharedLayers: sharedLayers
}).convert( themeConfigJson, "test").result
const themeConfig = new LayoutConfig(themeConfigJsonPrepared);
const layerUnderTest = <LayerConfig> themeConfig.layers.find(l => l.id === "public_bookcase")
expect(layerUnderTest.source.geojsonSource).eq("xyz")
})
})
describe("ExtractImages", () => {
it("should find all images in a themefile", () => {
const images = new Set(new ExtractImages(true, new Map<string, any>()).convertStrict(<any> cyclofix, "test"))
const expectedValues = [
'./assets/layers/bike_repair_station/repair_station.svg',
'./assets/layers/bike_repair_station/repair_station_pump.svg',
'./assets/layers/bike_repair_station/broken_pump.svg',
'./assets/layers/bike_repair_station/pump.svg',
'./assets/themes/cyclofix/fietsambassade_gent_logo_small.svg',
'./assets/layers/bike_repair_station/pump_example_manual.jpg',
'./assets/layers/bike_repair_station/pump_example.png',
'./assets/layers/bike_repair_station/pump_example_round.jpg',
'./assets/layers/bike_repair_station/repair_station_example_2.jpg',
'close']
for (const expected of expectedValues) {
expect(images).contains(expected)
}
})
})