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
|
@ -13,6 +13,7 @@ import {GeoOperations} from "../../GeoOperations";
|
||||||
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
|
|
||||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||||
|
public readonly state = new UIEventSource<undefined | {error: string} | "loaded">(undefined)
|
||||||
public readonly name;
|
public readonly name;
|
||||||
public readonly isOsmCache: boolean
|
public readonly isOsmCache: boolean
|
||||||
public readonly layer: FilteredLayer;
|
public readonly layer: FilteredLayer;
|
||||||
|
@ -80,6 +81,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
const self = this;
|
const self = this;
|
||||||
Utils.downloadJson(url)
|
Utils.downloadJson(url)
|
||||||
.then(json => {
|
.then(json => {
|
||||||
|
self.state.setData("loaded")
|
||||||
if (json.features === undefined || json.features === null) {
|
if (json.features === undefined || json.features === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +137,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
|
|
||||||
eventSource.setData(eventSource.data.concat(newFeatures))
|
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})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||||
import {UIEventSource} from "../../UIEventSource";
|
import {UIEventSource} from "../../UIEventSource";
|
||||||
import Loc from "../../../Models/Loc";
|
|
||||||
import DynamicTileSource from "./DynamicTileSource";
|
import DynamicTileSource from "./DynamicTileSource";
|
||||||
import {Utils} from "../../../Utils";
|
import {Utils} from "../../../Utils";
|
||||||
import GeoJsonSource from "../Sources/GeoJsonSource";
|
import GeoJsonSource from "../Sources/GeoJsonSource";
|
||||||
|
@ -14,7 +13,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||||
constructor(layer: FilteredLayer,
|
constructor(layer: FilteredLayer,
|
||||||
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
||||||
state: {
|
state: {
|
||||||
locationControl: UIEventSource<Loc>
|
locationControl?: UIEventSource<{zoom?: number}>
|
||||||
currentBounds: UIEventSource<BBox>
|
currentBounds: UIEventSource<BBox>
|
||||||
}) {
|
}) {
|
||||||
const source = layer.layerDef.source
|
const source = layer.layerDef.source
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||||
import {UIEventSource} from "../../UIEventSource";
|
import {UIEventSource} from "../../UIEventSource";
|
||||||
import Loc from "../../../Models/Loc";
|
|
||||||
import TileHierarchy from "./TileHierarchy";
|
import TileHierarchy from "./TileHierarchy";
|
||||||
import {Tiles} from "../../../Models/TileRange";
|
import {Tiles} from "../../../Models/TileRange";
|
||||||
import {BBox} from "../../BBox";
|
import {BBox} from "../../BBox";
|
||||||
|
@ -19,30 +18,29 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
|
||||||
constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
|
constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
|
||||||
state: {
|
state: {
|
||||||
currentBounds: UIEventSource<BBox>;
|
currentBounds: UIEventSource<BBox>;
|
||||||
locationControl: UIEventSource<Loc>
|
locationControl?: UIEventSource<{zoom?: number}>
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>()
|
this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>()
|
||||||
const neededTiles = state.locationControl.map(
|
const neededTiles = state.currentBounds.map(
|
||||||
location => {
|
bounds => {
|
||||||
|
if (bounds === undefined) {
|
||||||
|
// We'll retry later
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
|
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
|
||||||
// No need to download! - the layer is disabled
|
// No need to download! - the layer is disabled
|
||||||
return undefined;
|
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
|
// No need to download! - the layer is disabled
|
||||||
return undefined;
|
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())
|
const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
|
||||||
if (tileRange.total > 10000) {
|
if (tileRange.total > 10000) {
|
||||||
console.error("Got a really big tilerange, bounds and location might be out of sync")
|
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
|
return needed
|
||||||
}
|
}
|
||||||
, [layer.isDisplayed, state.currentBounds]).stabilized(250);
|
, [layer.isDisplayed, state.locationControl]).stabilized(250);
|
||||||
|
|
||||||
neededTiles.addCallbackAndRunD(neededIndexes => {
|
neededTiles.addCallbackAndRunD(neededIndexes => {
|
||||||
console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes)
|
console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes)
|
||||||
|
|
|
@ -8,18 +8,24 @@ import {Utils} from "../../Utils";
|
||||||
export class IdbLocalStorage {
|
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)
|
const src = new UIEventSource<T>(options?.defaultValue, "idb-local-storage:" + key)
|
||||||
if (Utils.runningFromConsole) {
|
if (Utils.runningFromConsole) {
|
||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
|
src.addCallback(v => idb.set(key, v))
|
||||||
|
|
||||||
idb.get(key).then(v => {
|
idb.get(key).then(v => {
|
||||||
src.setData(v ?? options?.defaultValue);
|
src.setData(v ?? options?.defaultValue);
|
||||||
if (options?.whenLoaded !== undefined) {
|
if (options?.whenLoaded !== undefined) {
|
||||||
options?.whenLoaded(v)
|
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;
|
return src;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,14 @@ import {DropDown} from "../Input/DropDown";
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
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<{
|
export class AskMetadata extends Combine implements FlowStep<{
|
||||||
features: any[],
|
features: any[],
|
||||||
|
@ -27,14 +35,14 @@ export class AskMetadata extends Combine implements FlowStep<{
|
||||||
}>;
|
}>;
|
||||||
public readonly IsValid: UIEventSource<boolean>;
|
public readonly IsValid: UIEventSource<boolean>;
|
||||||
|
|
||||||
constructor(params: ({ features: any[], layer: LayerConfig })) {
|
constructor(params: ({ features: any[], theme: string })) {
|
||||||
|
|
||||||
const introduction = ValidatedTextField.ForType("text").ConstructInputElement({
|
const introduction = ValidatedTextField.ForType("text").ConstructInputElement({
|
||||||
value: LocalStorageSource.Get("import-helper-introduction-text"),
|
value: LocalStorageSource.Get("import-helper-introduction-text"),
|
||||||
inputStyle: "width: 100%"
|
inputStyle: "width: 100%"
|
||||||
})
|
})
|
||||||
|
|
||||||
const wikilink = ValidatedTextField.ForType("string").ConstructInputElement({
|
const wikilink = ValidatedTextField.ForType("url").ConstructInputElement({
|
||||||
value: LocalStorageSource.Get("import-helper-wikilink-text"),
|
value: LocalStorageSource.Get("import-helper-wikilink-text"),
|
||||||
inputStyle: "width: 100%"
|
inputStyle: "width: 100%"
|
||||||
})
|
})
|
||||||
|
@ -44,26 +52,6 @@ export class AskMetadata extends Combine implements FlowStep<{
|
||||||
inputStyle: "width: 100%"
|
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([
|
super([
|
||||||
new Title("Set metadata"),
|
new Title("Set metadata"),
|
||||||
"Before adding " + params.features.length + " notes, please provide some extra information.",
|
"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"),
|
source.SetClass("w-full border border-black"),
|
||||||
"On what wikipage can one find more information about this import?",
|
"On what wikipage can one find more information about this import?",
|
||||||
wikilink.SetClass("w-full border border-black"),
|
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")
|
this.SetClass("flex flex-col")
|
||||||
|
|
||||||
|
@ -83,16 +84,30 @@ export class AskMetadata extends Combine implements FlowStep<{
|
||||||
wikilink: wikilink.GetValue().data,
|
wikilink: wikilink.GetValue().data,
|
||||||
intro,
|
intro,
|
||||||
source: source.GetValue().data,
|
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 => {
|
this.IsValid = this.Value.map(obj => {
|
||||||
if (obj === undefined) {
|
if (obj === undefined) {
|
||||||
return false;
|
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 {BBox} from "../../Logic/BBox";
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {DesugaringContext} from "../../Models/ThemeConfig/Conversion/Conversion";
|
|
||||||
import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
|
import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
|
||||||
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||||
import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource";
|
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 * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json";
|
||||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||||
import Title from "../Base/Title";
|
import Title from "../Base/Title";
|
||||||
import Toggle from "../Input/Toggle";
|
|
||||||
import Loading from "../Base/Loading";
|
import Loading from "../Base/Loading";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
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
|
* 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 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]
|
const layerConfig = known_layers.layers.filter(l => l.id === params.layer.id)[0]
|
||||||
if (layerConfig === undefined) {
|
if (layerConfig === undefined) {
|
||||||
console.error("WEIRD: layer not found in the builtin layer overview")
|
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 importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic")
|
||||||
const flayer: FilteredLayer = {
|
const flayer: FilteredLayer = {
|
||||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()),
|
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()),
|
||||||
|
@ -47,12 +45,13 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
||||||
layerDef: importLayer
|
layerDef: importLayer
|
||||||
}
|
}
|
||||||
const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
|
const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
|
||||||
|
|
||||||
allNotesWithinBbox.features.map(f => MetaTagging.addMetatags(
|
allNotesWithinBbox.features.map(f => MetaTagging.addMetatags(
|
||||||
f,
|
f,
|
||||||
{
|
{
|
||||||
memberships: new RelationsTracker(),
|
memberships: new RelationsTracker(),
|
||||||
getFeaturesWithin: (layerId, bbox: BBox) => [],
|
getFeaturesWithin: () => [],
|
||||||
getFeatureById: (id: string) => undefined
|
getFeatureById: () => undefined
|
||||||
},
|
},
|
||||||
importLayer,
|
importLayer,
|
||||||
state,
|
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)
|
.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 Title("Compare with already existing 'to-import'-notes"),
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
alreadyOpenImportNotes.features.map(notesWithImport => {
|
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) {
|
if (allNotesWithinBbox.features.data === undefined || allNotesWithinBbox.features.data.length === 0) {
|
||||||
return new Loading("Fetching notes from OSM")
|
return new Loading("Fetching notes from OSM")
|
||||||
}
|
}
|
||||||
if (notesWithImport.length === 0) {
|
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([
|
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,
|
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"),
|
return new Combine([
|
||||||
partitionedImportPoints.map(({noNearby}) => noNearby.length === 0)
|
new FixedUiElement(hasNearby.length+" points do have an already existing import note within "+maxDistance.data+" meter.").SetClass("alert"),
|
||||||
).SetClass("w-full"),
|
"These data points will <i>not</i> be imported and are shown as red dots on the map below",
|
||||||
comparisonMap,
|
comparisonMap.SetClass("w-full")
|
||||||
|
]).SetClass("w-full")
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
]).SetClass("flex flex-col")
|
]).SetClass("flex flex-col")
|
||||||
|
|
||||||
}, [allNotesWithinBbox.features])
|
}, [allNotesWithinBbox.features, allNotesWithinBbox.state])
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
this.SetClass("flex flex-col")
|
this.SetClass("flex flex-col")
|
||||||
this.Value = partitionedImportPoints.map(({noNearby}) => ({
|
this.Value = partitionedImportPoints.map(({noNearby}) => ({
|
||||||
geojson: {features: noNearby, type: "FeatureCollection"},
|
features: noNearby,
|
||||||
bbox: params.bbox,
|
bbox: params.bbox,
|
||||||
layer: params.layer
|
layer: params.layer,
|
||||||
|
theme: params.theme
|
||||||
}))
|
}))
|
||||||
|
|
||||||
this.IsValid = alreadyOpenImportNotes.features.map(ff => {
|
this.IsValid = alreadyOpenImportNotes.features.map(ff => {
|
||||||
|
|
|
@ -8,14 +8,13 @@ import Title from "../Base/Title";
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import {Utils} from "../../Utils";
|
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 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 = [
|
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 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",
|
type:"FeatureCollection",
|
||||||
features: v.features
|
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"
|
mimetype: "application/vnd.geo+json"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
this.SetClass("link-underline")
|
this.SetClass("link-underline")
|
||||||
this.IsValid = licenseClear.GetValue().map(selected => toConfirm.length == selected.length)
|
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
|
* 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 IsValid
|
||||||
public readonly Value
|
public readonly Value
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state,
|
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 bbox = params.bbox.padAbsolute(0.0001)
|
||||||
const layer = params.layer;
|
const layer = params.layer;
|
||||||
const toImport = params.geojson;
|
const toImport: {features: any[]} = params;
|
||||||
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle")
|
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle")
|
||||||
const cacheAge = new UIEventSource<number>(undefined);
|
const cacheAge = new UIEventSource<number>(undefined);
|
||||||
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
|
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
|
||||||
|
|
||||||
whenLoaded: (v) => {
|
whenLoaded: (v) => {
|
||||||
if (v !== undefined) {
|
if (v !== undefined && v !== null) {
|
||||||
console.log("Loaded from local storage:", v)
|
console.log("Loaded from local storage:", v)
|
||||||
const [geojson, date] = v;
|
const [geojson, date] = v;
|
||||||
const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
|
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"),
|
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,
|
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 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 Title("Nearby features"),
|
||||||
new Combine(["The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
|
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",
|
"Set the range to 0 or 1 if you want to import them all",
|
||||||
matchedFeaturesMap]).SetClass("flex flex-col")
|
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 = 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)
|
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 {MapPreview} from "./MapPreview";
|
||||||
import LeftIndex from "../Base/LeftIndex";
|
import LeftIndex from "../Base/LeftIndex";
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
|
import SelectTheme from "./SelectTheme";
|
||||||
|
|
||||||
export default class ImportHelperGui extends LeftIndex {
|
export default class ImportHelperGui extends LeftIndex {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -31,14 +32,15 @@ export default class ImportHelperGui extends LeftIndex {
|
||||||
FlowPanelFactory
|
FlowPanelFactory
|
||||||
.start("Introduction", new Introdution())
|
.start("Introduction", new Introdution())
|
||||||
.then("Login", _ => new LoginToImport(state))
|
.then("Login", _ => new LoginToImport(state))
|
||||||
.then("Select file", _ => new RequestFile())
|
.then("Select file", _ => new RequestFile())
|
||||||
.then("Inspect attributes", geojson => new PreviewPanel(state, geojson))
|
.then("Inspect attributes", geojson => new PreviewPanel(state, geojson))
|
||||||
.then("Inspect data", geojson => new MapPreview(state, geojson))
|
.then("Inspect data", geojson => new MapPreview(state, geojson))
|
||||||
.then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v))
|
.then("Select theme", v => new SelectTheme(v))
|
||||||
.then("Compare with existing data", v => new ConflationChecker(state, v))
|
.then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v))
|
||||||
.then("License and community check", v => new ConfirmProcess(v))
|
.then("Compare with existing data", v => new ConflationChecker(state, v))
|
||||||
.then("Metadata", (v: { features: any[], layer: LayerConfig }) => new AskMetadata(v))
|
.then("License and community check", (v : {features: any[], theme: string}) => new ConfirmProcess(v))
|
||||||
.finish("Note creation", v => new CreateNotes(state, 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(
|
const toc = new List(
|
||||||
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
|
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Toggle from "../Input/Toggle";
|
||||||
import {SubtleButton} from "../Base/SubtleButton";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import MoreScreen from "../BigComponents/MoreScreen";
|
import MoreScreen from "../BigComponents/MoreScreen";
|
||||||
|
import CheckBoxes from "../Input/Checkboxes";
|
||||||
|
|
||||||
export default class LoginToImport extends Combine implements FlowStep<UserRelatedState> {
|
export default class LoginToImport extends Combine implements FlowStep<UserRelatedState> {
|
||||||
readonly IsValid: UIEventSource<boolean>;
|
readonly IsValid: UIEventSource<boolean>;
|
||||||
|
@ -21,7 +22,9 @@ export default class LoginToImport extends Combine implements FlowStep<UserRelat
|
||||||
|
|
||||||
constructor(state: UserRelatedState) {
|
constructor(state: UserRelatedState) {
|
||||||
const t = Translations.t.importHelper
|
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([
|
super([
|
||||||
new Title(t.userAccountTitle),
|
new Title(t.userAccountTitle),
|
||||||
new LoginToggle(
|
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"),
|
new Img(ud.img ?? "./assets/svgs/help.svg").SetClass("w-16 h-16 rounded-full"),
|
||||||
t.loggedInWith.Subs(ud),
|
t.loggedInWith.Subs(ud),
|
||||||
new SubtleButton(Svg.logout_svg().SetClass("h-8"), Translations.t.general.logout)
|
new SubtleButton(Svg.logout_svg().SetClass("h-8"), Translations.t.general.logout)
|
||||||
.onClick(() => state.osmConnection.LogOut())
|
.onClick(() => state.osmConnection.LogOut()),
|
||||||
|
check
|
||||||
]);
|
]);
|
||||||
})),
|
})),
|
||||||
t.loginRequired,
|
t.loginRequired,
|
||||||
|
@ -46,6 +50,6 @@ export default class LoginToImport extends Combine implements FlowStep<UserRelat
|
||||||
, isValid)
|
, isValid)
|
||||||
])
|
])
|
||||||
this.Value = new UIEventSource<UserRelatedState>(state)
|
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
|
* 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 IsValid: UIEventSource<boolean>;
|
||||||
public readonly Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, geojson: any }>
|
public readonly Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[] }>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: UserRelatedState,
|
state: UserRelatedState,
|
||||||
|
@ -153,7 +153,7 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer:
|
||||||
this.Value = bbox.map(bbox =>
|
this.Value = bbox.map(bbox =>
|
||||||
({
|
({
|
||||||
bbox,
|
bbox,
|
||||||
geojson,
|
features: geojson.features,
|
||||||
layer: layerPicker.GetValue().data
|
layer: layerPicker.GetValue().data
|
||||||
}), [layerPicker.GetValue()])
|
}), [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
|
* 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>
|
public readonly IsValid: UIEventSource<boolean>
|
||||||
/**
|
/**
|
||||||
* The loaded GeoJSON
|
* The loaded GeoJSON
|
||||||
*/
|
*/
|
||||||
public readonly Value: UIEventSource<any>
|
public readonly Value: UIEventSource<{features: any[]}>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const t = Translations.t.importHelper.selectFile;
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -280,7 +280,8 @@
|
||||||
"inspectLooksCorrect": "These values look correct",
|
"inspectLooksCorrect": "These values look correct",
|
||||||
"lockNotice": "This page is locked. You need {importHelperUnlock} changesets before you can access here.",
|
"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",
|
"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",
|
"loginRequired": "You have to be logged in to continue",
|
||||||
"mapPreview": {
|
"mapPreview": {
|
||||||
"autodetected": "The layer was automatically deducted based on the properties",
|
"autodetected": "The layer was automatically deducted based on the properties",
|
||||||
|
|
75
tests/Models/ThemeConfig/Conversion/PrepareLayer.spec.ts
Normal file
75
tests/Models/ThemeConfig/Conversion/PrepareLayer.spec.ts
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue