Reformat all files with prettier

This commit is contained in:
Pieter Vander Vennet 2022-09-08 21:40:48 +02:00
parent e22d189376
commit b541d3eab4
382 changed files with 50893 additions and 35566 deletions

View file

@ -1,117 +1,128 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {Store} from "../../Logic/UIEventSource";
import ValidatedTextField from "../Input/ValidatedTextField";
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
import Title from "../Base/Title";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
export class AskMetadata extends Combine implements FlowStep<{
features: any[],
wikilink: string,
intro: string,
source: string,
theme: string
}> {
import Combine from "../Base/Combine"
import { FlowStep } from "./FlowStep"
import { Store } from "../../Logic/UIEventSource"
import ValidatedTextField from "../Input/ValidatedTextField"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import Title from "../Base/Title"
import { VariableUiElement } from "../Base/VariableUIElement"
import Translations from "../i18n/Translations"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import { Utils } from "../../Utils"
export class AskMetadata
extends Combine
implements
FlowStep<{
features: any[]
wikilink: string
intro: string
source: string
theme: string
}>
{
public readonly Value: Store<{
features: any[],
wikilink: string,
intro: string,
source: string,
features: any[]
wikilink: string
intro: string
source: string
theme: string
}>;
public readonly IsValid: Store<boolean>;
}>
public readonly IsValid: Store<boolean>
constructor(params: ({ features: any[], theme: string })) {
constructor(params: { features: any[]; theme: string }) {
const t = Translations.t.importHelper.askMetadata
const introduction = ValidatedTextField.ForType("text").ConstructInputElement({
value: LocalStorageSource.Get("import-helper-introduction-text"),
inputStyle: "width: 100%"
inputStyle: "width: 100%",
})
const wikilink = ValidatedTextField.ForType("url").ConstructInputElement({
value: LocalStorageSource.Get("import-helper-wikilink-text"),
inputStyle: "width: 100%"
inputStyle: "width: 100%",
})
const source = ValidatedTextField.ForType("string").ConstructInputElement({
value: LocalStorageSource.Get("import-helper-source-text"),
inputStyle: "width: 100%"
inputStyle: "width: 100%",
})
super([
new Title(t.title),
t.intro.Subs({count: params.features.length}),
t.giveDescription,
t.intro.Subs({ count: params.features.length }),
t.giveDescription,
introduction.SetClass("w-full border border-black"),
t.giveSource,
source.SetClass("w-full border border-black"),
t.giveWikilink ,
t.giveSource,
source.SetClass("w-full border border-black"),
t.giveWikilink,
wikilink.SetClass("w-full border border-black"),
new VariableUiElement(wikilink.GetValue().map(wikilink => {
try{
const url = new URL(wikilink)
if(url.hostname.toLowerCase() !== "wiki.openstreetmap.org"){
return t.shouldBeOsmWikilink.SetClass("alert");
}
new VariableUiElement(
wikilink.GetValue().map((wikilink) => {
try {
const url = new URL(wikilink)
if (url.hostname.toLowerCase() !== "wiki.openstreetmap.org") {
return t.shouldBeOsmWikilink.SetClass("alert")
}
if(url.pathname.toLowerCase() === "/wiki/main_page"){
return t.shouldNotBeHomepage.SetClass("alert");
if (url.pathname.toLowerCase() === "/wiki/main_page") {
return t.shouldNotBeHomepage.SetClass("alert")
}
} catch (e) {
return t.shouldBeUrl.SetClass("alert")
}
}catch(e){
return t.shouldBeUrl.SetClass("alert")
}
})),
t.orDownload,
new SubtleButton(Svg.download_svg(), t.downloadGeojson).OnClickWithLoading("Preparing your download",
async ( ) => {
const geojson = {
type:"FeatureCollection",
features: params.features
}
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "prepared_import_"+params.theme+".geojson",{
mimetype: "application/vnd.geo+json"
})
})
]);
),
t.orDownload,
new SubtleButton(Svg.download_svg(), t.downloadGeojson).OnClickWithLoading(
"Preparing your download",
async () => {
const geojson = {
type: "FeatureCollection",
features: params.features,
}
Utils.offerContentsAsDownloadableFile(
JSON.stringify(geojson),
"prepared_import_" + params.theme + ".geojson",
{
mimetype: "application/vnd.geo+json",
}
)
}
),
])
this.SetClass("flex flex-col")
this.Value = introduction.GetValue().map(intro => {
return {
features: params.features,
wikilink: wikilink.GetValue().data,
intro,
source: source.GetValue().data,
theme: params.theme
}
}, [wikilink.GetValue(), source.GetValue()])
this.IsValid = this.Value.map(obj => {
if (obj === undefined) {
return false;
}
if ([ obj.features, obj.intro, obj.wikilink, obj.source].some(v => v === undefined)){
return false;
}
try{
const url = new URL(obj.wikilink)
if(url.hostname.toLowerCase() !== "wiki.openstreetmap.org"){
return false;
this.Value = introduction.GetValue().map(
(intro) => {
return {
features: params.features,
wikilink: wikilink.GetValue().data,
intro,
source: source.GetValue().data,
theme: params.theme,
}
}catch(e){
},
[wikilink.GetValue(), source.GetValue()]
)
this.IsValid = this.Value.map((obj) => {
if (obj === undefined) {
return false
}
return true;
if ([obj.features, obj.intro, obj.wikilink, obj.source].some((v) => v === undefined)) {
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

@ -1,74 +1,84 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {BBox} from "../../Logic/BBox";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource";
import MetaTagging from "../../Logic/MetaTagging";
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource";
import Minimap from "../Base/Minimap";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import {ImportUtils} from "./ImportUtils";
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import Title from "../Base/Title";
import Loading from "../Base/Loading";
import {VariableUiElement} from "../Base/VariableUIElement";
import Combine from "../Base/Combine"
import { FlowStep } from "./FlowStep"
import { BBox } from "../../Logic/BBox"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource"
import MetaTagging from "../../Logic/MetaTagging"
import RelationsTracker from "../../Logic/Osm/RelationsTracker"
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import FeatureInfoBox from "../Popup/FeatureInfoBox"
import { ImportUtils } from "./ImportUtils"
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import Title from "../Base/Title"
import Loading from "../Base/Loading"
import { VariableUiElement } from "../Base/VariableUIElement"
import * as known_layers from "../../assets/generated/known_layers.json"
import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson";
import Translations from "../i18n/Translations";
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import Translations from "../i18n/Translations"
/**
* Filters out points for which the import-note already exists, to prevent duplicates
*/
export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> {
export class CompareToAlreadyExistingNotes
extends Combine
implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }>
{
public IsValid: Store<boolean>
public Value: Store<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }>
public Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }>
constructor(state, params: { bbox: BBox, layer: LayerConfig, features: any[], theme: string }) {
constructor(state, params: { bbox: BBox; layer: LayerConfig; features: any[]; theme: string }) {
const t = Translations.t.importHelper.compareToAlreadyExistingNotes
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) {
console.error("WEIRD: layer not found in the builtin layer overview")
}
const importLayerJson = new CreateNoteImportLayer(150).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>()),
appliedFilters: new UIEventSource<Map<string, FilterState>>(
new Map<string, FilterState>()
),
isDisplayed: new UIEventSource<boolean>(true),
layerDef: importLayer
layerDef: importLayer,
}
const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
allNotesWithinBbox.features.map(f => MetaTagging.addMetatags(
allNotesWithinBbox.features.map((f) =>
MetaTagging.addMetatags(
f,
{
memberships: new RelationsTracker(),
getFeaturesWithin: () => [],
getFeatureById: () => undefined
getFeatureById: () => undefined,
},
importLayer,
state,
{
includeDates: true,
// We assume that the non-dated metatags are already set by the cache generator
includeNonDates: true
includeNonDates: true,
}
)
)
const alreadyOpenImportNotes = new FilteringFeatureSource(state, undefined, allNotesWithinBbox)
const alreadyOpenImportNotes = new FilteringFeatureSource(
state,
undefined,
allNotesWithinBbox
)
const map = Minimap.createMiniMap()
map.SetClass("w-full").SetStyle("height: 500px")
const comparisonMap = Minimap.createMiniMap({
location: map.location,
})
comparisonMap.SetClass("w-full").SetStyle("height: 500px")
@ -78,94 +88,109 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
zoomToFeatures: true,
leafletMap: map.leafletMap,
features: alreadyOpenImportNotes,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state)
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
})
const maxDistance = new UIEventSource<number>(10)
const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(params, alreadyOpenImportNotes.features
.map(ff => ({features: ff.map(ff => ff.feature)})), maxDistance)
const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(
params,
alreadyOpenImportNotes.features.map((ff) => ({ features: ff.map((ff) => ff.feature) })),
maxDistance
)
new ShowDataLayer({
layerToShow: new LayerConfig(import_candidate),
state,
zoomToFeatures: true,
leafletMap: comparisonMap.leafletMap,
features: StaticFeatureSource.fromGeojsonStore(partitionedImportPoints.map(p => p.hasNearby)),
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state)
features: StaticFeatureSource.fromGeojsonStore(
partitionedImportPoints.map((p) => p.hasNearby)
),
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
})
super([
new Title(t.titleLong),
new VariableUiElement(
alreadyOpenImportNotes.features.map(notesWithImport => {
if (allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined) {
const error = allNotesWithinBbox.state.data["error"]
t.loadingFailed.Subs({error})
}
if (allNotesWithinBbox.features.data === undefined || allNotesWithinBbox.features.data.length === 0) {
return new Loading(t.loading)
}
if (notesWithImport.length === 0) {
return t.noPreviousNotesFound.SetClass("thanks")
}
return new Combine([
t.mapExplanation.Subs(params.features),
map,
alreadyOpenImportNotes.features.map(
(notesWithImport) => {
if (
allNotesWithinBbox.state.data !== undefined &&
allNotesWithinBbox.state.data["error"] !== undefined
) {
const error = allNotesWithinBbox.state.data["error"]
t.loadingFailed.Subs({ error })
}
if (
allNotesWithinBbox.features.data === undefined ||
allNotesWithinBbox.features.data.length === 0
) {
return new Loading(t.loading)
}
if (notesWithImport.length === 0) {
return t.noPreviousNotesFound.SetClass("thanks")
}
return new Combine([
t.mapExplanation.Subs(params.features),
map,
new VariableUiElement(partitionedImportPoints.map(({noNearby, hasNearby}) => {
new VariableUiElement(
partitionedImportPoints.map(({ noNearby, hasNearby }) => {
if (noNearby.length === 0) {
// Nothing can be imported
return t.completelyImported
.SetClass("alert w-full block")
.SetStyle("padding: 0.5rem")
}
if (noNearby.length === 0) {
// Nothing can be imported
return t.completelyImported.SetClass("alert w-full block").SetStyle("padding: 0.5rem")
}
if (hasNearby.length === 0) {
// All points can be imported
return t.nothingNearby
.SetClass("thanks w-full block")
.SetStyle("padding: 0.5rem")
}
if (hasNearby.length === 0) {
// All points can be imported
return t.nothingNearby.SetClass("thanks w-full block").SetStyle("padding: 0.5rem")
}
return new Combine([
t.someNearby.Subs({
hasNearby: hasNearby.length,
distance: maxDistance.data
}).SetClass("alert"),
t.wontBeImported,
comparisonMap.SetClass("w-full")
]).SetClass("w-full")
}))
]).SetClass("flex flex-col")
}, [allNotesWithinBbox.features, allNotesWithinBbox.state])
return new Combine([
t.someNearby
.Subs({
hasNearby: hasNearby.length,
distance: maxDistance.data,
})
.SetClass("alert"),
t.wontBeImported,
comparisonMap.SetClass("w-full"),
]).SetClass("w-full")
})
),
]).SetClass("flex flex-col")
},
[allNotesWithinBbox.features, allNotesWithinBbox.state]
)
),
]);
])
this.SetClass("flex flex-col")
this.Value = partitionedImportPoints.map(({noNearby}) => ({
this.Value = partitionedImportPoints.map(({ noNearby }) => ({
features: noNearby,
bbox: params.bbox,
layer: params.layer,
theme: params.theme
theme: params.theme,
}))
this.IsValid = alreadyOpenImportNotes.features.map(ff => {
if (allNotesWithinBbox.features.data.length === 0) {
// Not yet loaded
return false
}
if (ff.length == 0) {
// No import notes at all
return true;
}
this.IsValid = alreadyOpenImportNotes.features.map(
(ff) => {
if (allNotesWithinBbox.features.data.length === 0) {
// Not yet loaded
return false
}
if (ff.length == 0) {
// No import notes at all
return true
}
return partitionedImportPoints.data.noNearby.length > 0; // at least _something_ can be imported
}, [partitionedImportPoints, allNotesWithinBbox.features])
return partitionedImportPoints.data.noNearby.length > 0 // at least _something_ can be imported
},
[partitionedImportPoints, allNotesWithinBbox.features]
)
}
}
}

View file

@ -1,32 +1,35 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Link from "../Base/Link";
import CheckBoxes from "../Input/Checkboxes";
import Title from "../Base/Title";
import Translations from "../i18n/Translations";
export class ConfirmProcess extends Combine implements FlowStep<{ features: any[], theme: string }> {
import Combine from "../Base/Combine"
import { FlowStep } from "./FlowStep"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Link from "../Base/Link"
import CheckBoxes from "../Input/Checkboxes"
import Title from "../Base/Title"
import Translations from "../i18n/Translations"
export class ConfirmProcess
extends Combine
implements FlowStep<{ features: any[]; theme: string }>
{
public IsValid: Store<boolean>
public Value: Store<{ features: any[], theme: string }>
public Value: Store<{ features: any[]; theme: string }>
constructor(v: { features: any[], theme: string }) {
const t = Translations.t.importHelper.confirmProcess;
constructor(v: { features: any[]; theme: string }) {
const t = Translations.t.importHelper.confirmProcess
const elements = [
new Link(t.readImportGuidelines, "https://wiki.openstreetmap.org/wiki/Import_guidelines", true),
new Link(
t.readImportGuidelines,
"https://wiki.openstreetmap.org/wiki/Import_guidelines",
true
),
t.contactedCommunity,
t.licenseIsCompatible,
t.wikipageIsMade
t.wikipageIsMade,
]
const toConfirm = new CheckBoxes(elements);
const toConfirm = new CheckBoxes(elements)
super([
new Title(t.titleLong),
toConfirm,
]);
super([new Title(t.titleLong), toConfirm])
this.SetClass("link-underline")
this.IsValid = toConfirm.GetValue().map(selected => elements.length == selected.length)
this.Value = new UIEventSource<{ features: any[], theme: string }>(v)
this.IsValid = toConfirm.GetValue().map((selected) => elements.length == selected.length)
this.Value = new UIEventSource<{ features: any[]; theme: string }>(v)
}
}
}

View file

@ -1,120 +1,134 @@
import {BBox} from "../../Logic/BBox";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
import {Overpass} from "../../Logic/Osm/Overpass";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Constants from "../../Models/Constants";
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FlowStep} from "./FlowStep";
import Loading from "../Base/Loading";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
import {IdbLocalStorage} from "../../Logic/Web/IdbLocalStorage";
import Minimap from "../Base/Minimap";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import Loc from "../../Models/Loc";
import Attribution from "../BigComponents/Attribution";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import ValidatedTextField from "../Input/ValidatedTextField";
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
import { BBox } from "../../Logic/BBox"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
import { Overpass } from "../../Logic/Osm/Overpass"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
import RelationsTracker from "../../Logic/Osm/RelationsTracker"
import { VariableUiElement } from "../Base/VariableUIElement"
import { FlowStep } from "./FlowStep"
import Loading from "../Base/Loading"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import { Utils } from "../../Utils"
import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage"
import Minimap from "../Base/Minimap"
import BaseLayer from "../../Models/BaseLayer"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import Loc from "../../Models/Loc"
import Attribution from "../BigComponents/Attribution"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import ValidatedTextField from "../Input/ValidatedTextField"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
import {GeoOperations} from "../../Logic/GeoOperations";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import {ImportUtils} from "./ImportUtils";
import Translations from "../i18n/Translations";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
import {Feature, FeatureCollection} from "@turf/turf";
import { GeoOperations } from "../../Logic/GeoOperations"
import FeatureInfoBox from "../Popup/FeatureInfoBox"
import { ImportUtils } from "./ImportUtils"
import Translations from "../i18n/Translations"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import { Feature, FeatureCollection } from "@turf/turf"
import * as currentview from "../../assets/layers/current_view/current_view.json"
import {CheckBox} from "../Input/Checkboxes";
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch";
import { CheckBox } from "../Input/Checkboxes"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
/**
* 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[], theme: string }> {
export default class ConflationChecker
extends Combine
implements FlowStep<{ features: any[]; theme: string }>
{
public readonly IsValid
public readonly Value: Store<{ features: any[], theme: string }>
constructor(
state,
params: { bbox: BBox, layer: LayerConfig, theme: string, features: any[] }) {
public readonly Value: Store<{ features: any[]; theme: string }>
constructor(state, params: { bbox: BBox; layer: LayerConfig; theme: string; features: any[] }) {
const t = Translations.t.importHelper.conflationChecker
const bbox = params.bbox.padAbsolute(0.0001)
const layer = params.layer;
const toImport: { features: any[] } = params;
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle")
const layer = params.layer
const toImport: { features: any[] } = params
let overpassStatus = new UIEventSource<
{ error: string } | "running" | "success" | "idle" | "cached"
>("idle")
function loadDataFromOverpass() {
// Load the data!
const url = Constants.defaultOverpassUrls[1]
const relationTracker = new RelationsTracker()
const overpass = new Overpass(params.layer.source.osmTags, [], url, new UIEventSource<number>(180), relationTracker, true)
const overpass = new Overpass(
params.layer.source.osmTags,
[],
url,
new UIEventSource<number>(180),
relationTracker,
true
)
console.log("Loading from overpass!")
overpassStatus.setData("running")
overpass.queryGeoJson(bbox).then(
([data, date]) => {
console.log("Received overpass-data: ", data.features.length, "features are loaded at ", date);
console.log(
"Received overpass-data: ",
data.features.length,
"features are loaded at ",
date
)
overpassStatus.setData("success")
fromLocalStorage.setData([data, date])
},
(error) => {
overpassStatus.setData({error})
})
overpassStatus.setData({ error })
}
)
}
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
whenLoaded: (v) => {
if (v !== undefined && v !== null) {
console.log("Loaded from local storage:", v)
overpassStatus.setData("cached")
}else{
loadDataFromOverpass()
}
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>(
"importer-overpass-cache-" + layer.id,
{
whenLoaded: (v) => {
if (v !== undefined && v !== null) {
console.log("Loaded from local storage:", v)
overpassStatus.setData("cached")
} else {
loadDataFromOverpass()
}
},
}
});
)
const cacheAge = fromLocalStorage.map(d => {
if(d === undefined || d[1] === undefined){
const cacheAge = fromLocalStorage.map((d) => {
if (d === undefined || d[1] === undefined) {
return undefined
}
const [_, loadedDate] = d
return (new Date().getTime() - loadedDate.getTime()) / 1000;
return (new Date().getTime() - loadedDate.getTime()) / 1000
})
cacheAge.addCallbackD(timeDiff => {
cacheAge.addCallbackD((timeDiff) => {
if (timeDiff < 24 * 60 * 60) {
// Recently cached!
// Recently cached!
overpassStatus.setData("cached")
return;
return
} else {
loadDataFromOverpass()
}
})
const geojson: Store<FeatureCollection> = fromLocalStorage.map(d => {
const geojson: Store<FeatureCollection> = fromLocalStorage.map((d) => {
if (d === undefined) {
return undefined
}
return d[0]
})
const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
const currentBounds = new UIEventSource<BBox>(undefined)
const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({
value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0")
value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0"),
})
zoomLevel.SetClass("ml-1 border border-black")
const osmLiveData = Minimap.createMiniMap({
@ -122,26 +136,34 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
location,
background,
bounds: currentBounds,
attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds)
attribution: new Attribution(
location,
state.osmConnection.userDetails,
undefined,
currentBounds
),
})
osmLiveData.SetClass("w-full").SetStyle("height: 500px")
const geojsonFeatures : Store<Feature[]> = geojson.map(geojson => {
if (geojson?.features === undefined) {
return []
}
const currentZoom = zoomLevel.GetValue().data
const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom)
if (currentZoom !== undefined && !zoomedEnough) {
return []
}
const bounds = osmLiveData.bounds.data
if(bounds === undefined){
return geojson.features;
}
return geojson.features.filter(f => BBox.get(f).overlapsWith(bounds))
}, [osmLiveData.bounds, zoomLevel.GetValue()])
const geojsonFeatures: Store<Feature[]> = geojson.map(
(geojson) => {
if (geojson?.features === undefined) {
return []
}
const currentZoom = zoomLevel.GetValue().data
const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom)
if (currentZoom !== undefined && !zoomedEnough) {
return []
}
const bounds = osmLiveData.bounds.data
if (bounds === undefined) {
return geojson.features
}
return geojson.features.filter((f) => BBox.get(f).overlapsWith(bounds))
},
[osmLiveData.bounds, zoomLevel.GetValue()]
)
const preview = StaticFeatureSource.fromGeojsonStore(geojsonFeatures)
new ShowDataLayer({
@ -150,32 +172,32 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
leafletMap: osmLiveData.leafletMap,
popup: undefined,
zoomToFeatures: true,
features: StaticFeatureSource.fromGeojson([
bbox.asGeoJson({})
])
features: StaticFeatureSource.fromGeojson([bbox.asGeoJson({})]),
})
new ShowDataMultiLayer({
//layerToShow: layer,
layers: new UIEventSource<FilteredLayer[]>([{
layerDef: layer,
isDisplayed: new UIEventSource<boolean>(true),
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
}]),
layers: new UIEventSource<FilteredLayer[]>([
{
layerDef: layer,
isDisplayed: new UIEventSource<boolean>(true),
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined),
},
]),
state,
leafletMap: osmLiveData.leafletMap,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
zoomToFeatures: false,
features: preview
features: preview,
})
new ShowDataLayer({
layerToShow: new LayerConfig(import_candidate),
state,
leafletMap: osmLiveData.leafletMap,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
zoomToFeatures: false,
features: StaticFeatureSource.fromGeojson(toImport.features)
features: StaticFeatureSource.fromGeojson(toImport.features),
})
const nearbyCutoff = ValidatedTextField.ForType("pnat").ConstructInputElement()
@ -184,138 +206,172 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
const matchedFeaturesMap = Minimap.createMiniMap({
allowMoving: true,
background
background,
})
matchedFeaturesMap.SetClass("w-full").SetStyle("height: 500px")
// Featuresource showing OSM-features which are nearby a toImport-feature
const geojsonMapped: Store<Feature[]> = geojson.map(osmData => {
if (osmData?.features === undefined) {
return []
}
const maxDist = Number(nearbyCutoff.GetValue().data)
return osmData.features.filter(f =>
toImport.features.some(imp =>
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))))
}, [nearbyCutoff.GetValue().stabilized(500)])
const nearbyFeatures = StaticFeatureSource.fromGeojsonStore(geojsonMapped);
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
// Featuresource showing OSM-features which are nearby a toImport-feature
const geojsonMapped: Store<Feature[]> = geojson.map(
(osmData) => {
if (osmData?.features === undefined) {
return []
}
const maxDist = Number(nearbyCutoff.GetValue().data)
return osmData.features.filter((f) =>
toImport.features.some(
(imp) =>
maxDist >=
GeoOperations.distanceBetween(
imp.geometry.coordinates,
GeoOperations.centerpointCoordinates(f)
)
)
)
},
[nearbyCutoff.GetValue().stabilized(500)]
)
const nearbyFeatures = StaticFeatureSource.fromGeojsonStore(geojsonMapped)
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(
toImport,
geojson,
nearbyCutoff.GetValue().map(Number)
)
// Featuresource showing OSM-features which are nearby a toImport-feature
const toImportWithNearby = StaticFeatureSource.fromGeojsonStore(paritionedImport.map(els => els?.hasNearby ?? []));
toImportWithNearby.features.addCallback(nearby => console.log("The following features are near an already existing object:", nearby))
// Featuresource showing OSM-features which are nearby a toImport-feature
const toImportWithNearby = StaticFeatureSource.fromGeojsonStore(
paritionedImport.map((els) => els?.hasNearby ?? [])
)
toImportWithNearby.features.addCallback((nearby) =>
console.log("The following features are near an already existing object:", nearby)
)
new ShowDataLayer({
layerToShow: new LayerConfig(import_candidate),
state,
leafletMap: matchedFeaturesMap.leafletMap,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
zoomToFeatures: false,
features: toImportWithNearby
features: toImportWithNearby,
})
const showOsmLayer = new CheckBox(t.showOsmLayerInConflationMap, true)
new ShowDataLayer({
layerToShow: layer,
state,
leafletMap: matchedFeaturesMap.leafletMap,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}),
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
zoomToFeatures: true,
features: nearbyFeatures,
doShowLayer: showOsmLayer.GetValue()
doShowLayer: showOsmLayer.GetValue(),
})
const conflationMaps = new Combine([
new VariableUiElement(
geojson.map(geojson => {
geojson.map((geojson) => {
if (geojson === undefined) {
return undefined;
return undefined
}
return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick(() => {
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
mimetype: "application/json+geo"
})
});
})),
new VariableUiElement(cacheAge.map(age => {
if (age === undefined) {
return undefined;
}
if (age < 0) {
return t.cacheExpired
}
return new Combine([t.loadedDataAge.Subs({age: Utils.toHumanTime(age)}),
new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache)
.onClick(loadDataFromOverpass)
.SetClass("h-12")
])
})),
return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick(
() => {
Utils.offerContentsAsDownloadableFile(
JSON.stringify(geojson, null, " "),
"mapcomplete-" + layer.id + ".geojson",
{
mimetype: "application/json+geo",
}
)
}
)
})
),
new VariableUiElement(
cacheAge.map((age) => {
if (age === undefined) {
return undefined
}
if (age < 0) {
return t.cacheExpired
}
return new Combine([
t.loadedDataAge.Subs({ age: Utils.toHumanTime(age) }),
new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache)
.onClick(loadDataFromOverpass)
.SetClass("h-12"),
])
})
),
new Title(t.titleLive),
t.importCandidatesCount.Subs({count: toImport.features.length}),
new VariableUiElement(geojson.map(geojson => {
if (geojson?.features?.length === undefined || geojson?.features?.length === 0) {
return t.nothingLoaded.Subs(layer).SetClass("alert")
}
return new Combine([
t.osmLoaded.Subs({count: geojson.features.length, name: layer.name}),
])
})),
t.importCandidatesCount.Subs({ count: toImport.features.length }),
new VariableUiElement(
geojson.map((geojson) => {
if (
geojson?.features?.length === undefined ||
geojson?.features?.length === 0
) {
return t.nothingLoaded.Subs(layer).SetClass("alert")
}
return new Combine([
t.osmLoaded.Subs({ count: geojson.features.length, name: layer.name }),
])
})
),
osmLiveData,
new Combine([
t.zoomLevelSelection,
zoomLevel,
new VariableUiElement(osmLiveData.location.map(location => {
return t.zoomIn.Subs(<any>{current: location.zoom})
})),
new VariableUiElement(
osmLiveData.location.map((location) => {
return t.zoomIn.Subs(<any>{ current: location.zoom })
})
),
]).SetClass("flex"),
new Title(t.titleNearby),
new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"),
new VariableUiElement(toImportWithNearby.features.map(feats =>
t.nearbyWarn.Subs({count: feats.length}).SetClass("alert"))),
new VariableUiElement(
toImportWithNearby.features.map((feats) =>
t.nearbyWarn.Subs({ count: feats.length }).SetClass("alert")
)
),
t.setRangeToZero,
matchedFeaturesMap,
new Combine([
new BackgroundMapSwitch({backgroundLayer: background, locationControl: matchedFeaturesMap.location}, background),
showOsmLayer,
]).SetClass("flex")
new BackgroundMapSwitch(
{ backgroundLayer: background, locationControl: matchedFeaturesMap.location },
background
),
showOsmLayer,
]).SetClass("flex"),
]).SetClass("flex flex-col")
super([
new Title(t.title),
new VariableUiElement(overpassStatus.map(d => {
if (d === "idle") {
return new Loading(t.states.idle)
}
if (d === "running") {
return new Loading(t.states.running)
}
if (d["error"] !== undefined) {
return t.states.error.Subs({error: d["error"]}).SetClass("alert")
}
if (d === "cached") {
return conflationMaps
}
if (d === "success") {
return conflationMaps
}
return t.states.unexpected.Subs({state: d}).SetClass("alert")
}))
new VariableUiElement(
overpassStatus.map((d) => {
if (d === "idle") {
return new Loading(t.states.idle)
}
if (d === "running") {
return new Loading(t.states.running)
}
if (d["error"] !== undefined) {
return t.states.error.Subs({ error: d["error"] }).SetClass("alert")
}
if (d === "cached") {
return conflationMaps
}
if (d === "success") {
return conflationMaps
}
return t.states.unexpected.Subs({ state: d }).SetClass("alert")
})
),
])
this.Value = paritionedImport.map(feats => ({
this.Value = paritionedImport.map((feats) => ({
theme: params.theme,
features: feats?.noNearby,
layer: params.layer
layer: params.layer,
}))
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)
}
}
}

View file

@ -1,22 +1,21 @@
import Combine from "../Base/Combine";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {UIEventSource} from "../../Logic/UIEventSource";
import Title from "../Base/Title";
import Toggle from "../Input/Toggle";
import Loading from "../Base/Loading";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import {Translation} from "../i18n/Translation";
import Combine from "../Base/Combine"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import Title from "../Base/Title"
import Toggle from "../Input/Toggle"
import Loading from "../Base/Loading"
import { VariableUiElement } from "../Base/VariableUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation"
export class CreateNotes extends Combine {
public static createNoteContentsUi(feature: {properties: any, geometry: {coordinates: [number,number]}},
options: {wikilink: string; intro: string; source: string, theme: string }
): (Translation | string)[]{
public static createNoteContentsUi(
feature: { properties: any; geometry: { coordinates: [number, number] } },
options: { wikilink: string; intro: string; source: string; theme: string }
): (Translation | string)[] {
const src = feature.properties["source"] ?? feature.properties["src"] ?? options.source
delete feature.properties["source"]
delete feature.properties["src"]
@ -26,7 +25,7 @@ export class CreateNotes extends Combine {
delete feature.properties["note"]
}
const tags: string [] = []
const tags: string[] = []
for (const key in feature.properties) {
if (feature.properties[key] === null || feature.properties[key] === undefined) {
console.warn("Null or undefined key for ", feature.properties)
@ -35,7 +34,14 @@ export class CreateNotes extends Combine {
if (feature.properties[key] === "") {
continue
}
tags.push(key + "=" + (feature.properties[key]+"").replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
tags.push(
key +
"=" +
(feature.properties[key] + "")
.replace(/=/, "\\=")
.replace(/;/g, "\\;")
.replace(/\n/g, "\\n")
)
}
const lat = feature.geometry.coordinates[1]
const lon = feature.geometry.coordinates[0]
@ -43,82 +49,88 @@ export class CreateNotes extends Combine {
return [
options.intro,
extraNote,
note.datasource.Subs({source: src}),
note.datasource.Subs({ source: src }),
note.wikilink.Subs(options),
'',
"",
note.importEasily,
`https://mapcomplete.osm.be/${options.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
...tags]
...tags,
]
}
public static createNoteContents(feature: {properties: any, geometry: {coordinates: [number,number]}},
options: {wikilink: string; intro: string; source: string, theme: string }
): string[]{
return CreateNotes.createNoteContentsUi(feature, options).map(trOrStr => {
if(typeof trOrStr === "string"){
public static createNoteContents(
feature: { properties: any; geometry: { coordinates: [number, number] } },
options: { wikilink: string; intro: string; source: string; theme: string }
): string[] {
return CreateNotes.createNoteContentsUi(feature, options).map((trOrStr) => {
if (typeof trOrStr === "string") {
return trOrStr
}
return trOrStr.txt
})
}
constructor(state: { osmConnection: OsmConnection }, v: { features: any[]; wikilink: string; intro: string; source: string, theme: string }) {
const t = Translations.t.importHelper.createNotes;
constructor(
state: { osmConnection: OsmConnection },
v: { features: any[]; wikilink: string; intro: string; source: string; theme: string }
) {
const t = Translations.t.importHelper.createNotes
const createdNotes: UIEventSource<number[]> = new UIEventSource<number[]>([])
const failed = new UIEventSource<string[]>([])
const currentNote = createdNotes.map(n => n.length)
const currentNote = createdNotes.map((n) => n.length)
for (const f of v.features) {
const lat = f.geometry.coordinates[1]
const lon = f.geometry.coordinates[0]
const text = CreateNotes.createNoteContents(f, v).join("\n")
state.osmConnection.openNote(
lat, lon, text)
.then(({id}) => {
state.osmConnection.openNote(lat, lon, text).then(
({ id }) => {
createdNotes.data.push(id)
createdNotes.ping()
}, err => {
},
(err) => {
failed.data.push(err)
failed.ping()
})
}
)
}
super([
new Title(t.title),
t.loading ,
t.loading,
new Toggle(
new Loading(new VariableUiElement(currentNote.map(count => t.creating.Subs({
count, total: v.features.length
}
)))),
new Loading(
new VariableUiElement(
currentNote.map((count) =>
t.creating.Subs({
count,
total: v.features.length,
})
)
)
),
new Combine([
Svg.party_svg().SetClass("w-24"),
t.done.Subs({count: v.features.length}).SetClass("thanks"),
new SubtleButton(Svg.note_svg(),
t.openImportViewer , {
url: "import_viewer.html"
})
]
),
currentNote.map(count => count < v.features.length)
t.done.Subs({ count: v.features.length }).SetClass("thanks"),
new SubtleButton(Svg.note_svg(), t.openImportViewer, {
url: "import_viewer.html",
}),
]),
currentNote.map((count) => count < v.features.length)
),
new VariableUiElement(
failed.map((failed) => {
if (failed.length === 0) {
return undefined
}
return new Combine([
new FixedUiElement("Some entries failed").SetClass("alert"),
...failed,
]).SetClass("flex flex-col")
})
),
new VariableUiElement(failed.map(failed => {
if (failed.length === 0) {
return undefined
}
return new Combine([
new FixedUiElement("Some entries failed").SetClass("alert"),
...failed
]).SetClass("flex flex-col")
}))
])
this.SetClass("flex flex-col");
this.SetClass("flex flex-col")
}
}
}

View file

@ -1,13 +1,13 @@
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import BaseUIElement from "../BaseUIElement";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import BaseUIElement from "../BaseUIElement"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle from "../Input/Toggle"
import { UIElement } from "../UIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
export interface FlowStep<T> extends BaseUIElement {
readonly IsValid: Store<boolean>
@ -15,21 +15,31 @@ export interface FlowStep<T> extends BaseUIElement {
}
export class FlowPanelFactory<T> {
private _initial: FlowStep<any>;
private _steps: ((x: any) => FlowStep<any>)[];
private _stepNames: (string | BaseUIElement)[];
private _initial: FlowStep<any>
private _steps: ((x: any) => FlowStep<any>)[]
private _stepNames: (string | BaseUIElement)[]
private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) {
this._initial = initial;
this._steps = steps;
this._stepNames = stepNames;
private constructor(
initial: FlowStep<any>,
steps: ((x: any) => FlowStep<any>)[],
stepNames: (string | BaseUIElement)[]
) {
this._initial = initial
this._steps = steps
this._stepNames = stepNames
}
public static start<TOut>(name:{title: BaseUIElement}, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
public static start<TOut>(
name: { title: BaseUIElement },
step: FlowStep<TOut>
): FlowPanelFactory<TOut> {
return new FlowPanelFactory(step, [], [name.title])
}
public then<TOut>(name: string | {title: BaseUIElement}, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> {
public then<TOut>(
name: string | { title: BaseUIElement },
construct: (t: T) => FlowStep<TOut>
): FlowPanelFactory<TOut> {
return new FlowPanelFactory<TOut>(
this._initial,
this._steps.concat([construct]),
@ -37,25 +47,30 @@ export class FlowPanelFactory<T> {
)
}
public finish(name: string | BaseUIElement, construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)): {
flow: BaseUIElement,
furthestStep: UIEventSource<number>,
public finish(
name: string | BaseUIElement,
construct: (t: T, backButton?: BaseUIElement) => BaseUIElement
): {
flow: BaseUIElement
furthestStep: UIEventSource<number>
titles: (string | BaseUIElement)[]
} {
const furthestStep = new UIEventSource(0)
// Construct all the flowpanels step by step (in reverse order)
const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined)
const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(
(_) => undefined
)
nextConstr.push(construct)
for (let i = this._steps.length - 1; i >= 0; i--) {
const createFlowStep: (value) => FlowStep<any> = this._steps[i];
const isConfirm = i == this._steps.length - 1;
const createFlowStep: (value) => FlowStep<any> = this._steps[i]
const isConfirm = i == this._steps.length - 1
nextConstr[i] = (value, backButton) => {
const flowStep = createFlowStep(value)
furthestStep.setData(i + 1);
const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm);
panel.isActive.addCallbackAndRun(active => {
furthestStep.setData(i + 1)
const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm)
panel.isActive.addCallbackAndRun((active) => {
if (active) {
furthestStep.setData(i + 1);
furthestStep.setData(i + 1)
}
})
return panel
@ -63,32 +78,31 @@ export class FlowPanelFactory<T> {
}
const flow = new FlowPanel(this._initial, nextConstr[0])
flow.isActive.addCallbackAndRun(active => {
flow.isActive.addCallbackAndRun((active) => {
if (active) {
furthestStep.setData(0);
furthestStep.setData(0)
}
})
return {
flow,
furthestStep,
titles: this._stepNames
titles: this._stepNames,
}
}
}
export class FlowPanel<T> extends Toggle {
public isActive: UIEventSource<boolean>
constructor(
initial: (FlowStep<T>),
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
initial: FlowStep<T>,
constructNextstep: (input: T, backButton: BaseUIElement) => BaseUIElement,
backbutton?: BaseUIElement,
isConfirm = false
) {
const t = Translations.t.general;
const t = Translations.t.general
const currentStepActive = new UIEventSource(true);
const currentStepActive = new UIEventSource(true)
let nextStep: UIEventSource<BaseUIElement> = new UIEventSource<BaseUIElement>(undefined)
const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => {
@ -106,13 +120,13 @@ export class FlowPanel<T> extends Toggle {
backbutton,
new Toggle(
new SubtleButton(
isConfirm ? Svg.checkmark_svg() :
Svg.back_svg().SetStyle("transform: rotate(180deg);"),
isConfirm
? Svg.checkmark_svg()
: Svg.back_svg().SetStyle("transform: rotate(180deg);"),
isConfirm ? t.confirm : t.next
).onClick(() => {
try {
const v = initial.Value.data;
const v = initial.Value.data
nextStep.setData(constructNextstep(v, backButtonForNextStep))
currentStepActive.setData(false)
} catch (e) {
@ -123,24 +137,16 @@ export class FlowPanel<T> extends Toggle {
new SubtleButton(Svg.invalid_svg(), t.notValid),
initial.IsValid
),
new Toggle(
t.error.SetClass("alert"),
undefined,
isError),
new Toggle(t.error.SetClass("alert"), undefined, isError),
]).SetClass("flex w-full justify-end space-x-2"),
]
}
super(
new Combine(elements).SetClass("h-full flex flex-col justify-between"),
new VariableUiElement(nextStep),
currentStepActive
);
)
this.isActive = currentStepActive
}
}
}

View file

@ -1,77 +1,84 @@
import Combine from "../Base/Combine";
import Toggle from "../Input/Toggle";
import LanguagePicker from "../LanguagePicker";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import BaseUIElement from "../BaseUIElement";
import MinimapImplementation from "../Base/MinimapImplementation";
import Translations from "../i18n/Translations";
import {FlowPanelFactory} from "./FlowStep";
import {RequestFile} from "./RequestFile";
import {PreviewAttributesPanel} from "./PreviewPanel";
import ConflationChecker from "./ConflationChecker";
import {AskMetadata} from "./AskMetadata";
import {ConfirmProcess} from "./ConfirmProcess";
import {CreateNotes} from "./CreateNotes";
import {VariableUiElement} from "../Base/VariableUIElement";
import List from "../Base/List";
import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes";
import Introdution from "./Introdution";
import LoginToImport from "./LoginToImport";
import {MapPreview} from "./MapPreview";
import LeftIndex from "../Base/LeftIndex";
import {SubtleButton} from "../Base/SubtleButton";
import SelectTheme from "./SelectTheme";
import Combine from "../Base/Combine"
import Toggle from "../Input/Toggle"
import LanguagePicker from "../LanguagePicker"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import BaseUIElement from "../BaseUIElement"
import MinimapImplementation from "../Base/MinimapImplementation"
import Translations from "../i18n/Translations"
import { FlowPanelFactory } from "./FlowStep"
import { RequestFile } from "./RequestFile"
import { PreviewAttributesPanel } from "./PreviewPanel"
import ConflationChecker from "./ConflationChecker"
import { AskMetadata } from "./AskMetadata"
import { ConfirmProcess } from "./ConfirmProcess"
import { CreateNotes } from "./CreateNotes"
import { VariableUiElement } from "../Base/VariableUIElement"
import List from "../Base/List"
import { CompareToAlreadyExistingNotes } from "./CompareToAlreadyExistingNotes"
import Introdution from "./Introdution"
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() {
const state = new UserRelatedState(undefined)
const t = Translations.t.importHelper;
const {flow, furthestStep, titles} =
FlowPanelFactory
.start(t.introduction, new Introdution())
.then(t.login, _ => new LoginToImport(state))
.then(t.selectFile, _ => new RequestFile())
.then(t.previewAttributes, geojson => new PreviewAttributesPanel(state, geojson))
.then(t.mapPreview, geojson => new MapPreview(state, geojson))
.then(t.selectTheme, v => new SelectTheme(v))
.then(t.compareToAlreadyExistingNotes, v => new CompareToAlreadyExistingNotes(state, v))
.then(t.conflationChecker, v => new ConflationChecker(state, v))
.then(t.confirmProcess, v => new ConfirmProcess(v))
.then(t.askMetadata, (v) => new AskMetadata(v))
.finish(t.createNotes.title, v => new CreateNotes(state, v));
const t = Translations.t.importHelper
const { flow, furthestStep, titles } = FlowPanelFactory.start(
t.introduction,
new Introdution()
)
.then(t.login, (_) => new LoginToImport(state))
.then(t.selectFile, (_) => new RequestFile())
.then(t.previewAttributes, (geojson) => new PreviewAttributesPanel(state, geojson))
.then(t.mapPreview, (geojson) => new MapPreview(state, geojson))
.then(t.selectTheme, (v) => new SelectTheme(v))
.then(
t.compareToAlreadyExistingNotes,
(v) => new CompareToAlreadyExistingNotes(state, v)
)
.then(t.conflationChecker, (v) => new ConflationChecker(state, v))
.then(t.confirmProcess, (v) => new ConfirmProcess(v))
.then(t.askMetadata, (v) => new AskMetadata(v))
.finish(t.createNotes.title, (v) => new CreateNotes(state, v))
const toc = new List(
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
if (i > currentStep) {
return new Combine([title]).SetClass("subtle");
}
if (i == currentStep) {
return new Combine([title]).SetClass("font-bold");
}
if (i < currentStep) {
return title
}
})))
, true)
titles.map(
(title, i) =>
new VariableUiElement(
furthestStep.map((currentStep) => {
if (i > currentStep) {
return new Combine([title]).SetClass("subtle")
}
if (i == currentStep) {
return new Combine([title]).SetClass("font-bold")
}
if (i < currentStep) {
return title
}
})
)
),
true
)
const leftContents: BaseUIElement[] = [
new SubtleButton(undefined, t.gotoImportViewer, {
url: "import_viewer.html"
url: "import_viewer.html",
}),
toc,
new Toggle(t.testMode.SetClass("block alert"), undefined, state.featureSwitchIsTesting),
new LanguagePicker(Translations.t.importHelper.title.SupportedLanguages(), "")?.SetClass("mt-4 self-end flex-col"),
].map(el => el?.SetClass("pl-4"))
super(
leftContents,
flow)
new LanguagePicker(
Translations.t.importHelper.title.SupportedLanguages(),
""
)?.SetClass("mt-4 self-end flex-col"),
].map((el) => el?.SetClass("pl-4"))
super(leftContents, flow)
}
}
MinimapImplementation.initialize()
new ImportHelperGui().AttachTo("main")
new ImportHelperGui().AttachTo("main")

View file

@ -1,35 +1,44 @@
import {Store} from "../../Logic/UIEventSource";
import {GeoOperations} from "../../Logic/GeoOperations";
import {Feature, Geometry} from "@turf/turf";
import { Store } from "../../Logic/UIEventSource"
import { GeoOperations } from "../../Logic/GeoOperations"
import { Feature, Geometry } from "@turf/turf"
export class ImportUtils {
public static partitionFeaturesIfNearby(
toPartitionFeatureCollection: ({ features: Feature<Geometry>[] }),
toPartitionFeatureCollection: { features: Feature<Geometry>[] },
compareWith: Store<{ features: Feature[] }>,
cutoffDistanceInMeters: Store<number>)
: Store<{ hasNearby: Feature[], noNearby: Feature[] }> {
return compareWith.map(osmData => {
if (osmData?.features === undefined) {
return undefined
}
if (osmData.features.length === 0) {
return {noNearby: toPartitionFeatureCollection.features, hasNearby: []}
}
const maxDist = cutoffDistanceInMeters.data
const hasNearby = []
const noNearby = []
for (const toImportElement of toPartitionFeatureCollection.features) {
const hasNearbyFeature = osmData.features.some(f =>
maxDist >= GeoOperations.distanceBetween(<any> toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))
if (hasNearbyFeature) {
hasNearby.push(toImportElement)
} else {
noNearby.push(toImportElement)
cutoffDistanceInMeters: Store<number>
): Store<{ hasNearby: Feature[]; noNearby: Feature[] }> {
return compareWith.map(
(osmData) => {
if (osmData?.features === undefined) {
return undefined
}
}
if (osmData.features.length === 0) {
return { noNearby: toPartitionFeatureCollection.features, hasNearby: [] }
}
const maxDist = cutoffDistanceInMeters.data
return {hasNearby, noNearby}
}, [cutoffDistanceInMeters]);
const hasNearby = []
const noNearby = []
for (const toImportElement of toPartitionFeatureCollection.features) {
const hasNearbyFeature = osmData.features.some(
(f) =>
maxDist >=
GeoOperations.distanceBetween(
<any>toImportElement.geometry.coordinates,
GeoOperations.centerpointCoordinates(f)
)
)
if (hasNearbyFeature) {
hasNearby.push(toImportElement)
} else {
noNearby.push(toImportElement)
}
}
return { hasNearby, noNearby }
},
[cutoffDistanceInMeters]
)
}
}
}

View file

@ -1,56 +1,62 @@
import Combine from "../Base/Combine";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import {VariableUiElement} from "../Base/VariableUIElement";
import {Utils} from "../../Utils";
import {UIEventSource} from "../../Logic/UIEventSource";
import Title from "../Base/Title";
import Translations from "../i18n/Translations";
import Loading from "../Base/Loading";
import {FixedUiElement} from "../Base/FixedUiElement";
import Link from "../Base/Link";
import {DropDown} from "../Input/DropDown";
import BaseUIElement from "../BaseUIElement";
import ValidatedTextField from "../Input/ValidatedTextField";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Toggle, {ClickableToggle} from "../Input/Toggle";
import Table from "../Base/Table";
import LeftIndex from "../Base/LeftIndex";
import Toggleable, {Accordeon} from "../Base/Toggleable";
import TableOfContents from "../Base/TableOfContents";
import {LoginToggle} from "../Popup/LoginButton";
import {QueryParameters} from "../../Logic/Web/QueryParameters";
import Lazy from "../Base/Lazy";
import {Button} from "../Base/Button";
import Combine from "../Base/Combine"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import { VariableUiElement } from "../Base/VariableUIElement"
import { Utils } from "../../Utils"
import { UIEventSource } from "../../Logic/UIEventSource"
import Title from "../Base/Title"
import Translations from "../i18n/Translations"
import Loading from "../Base/Loading"
import { FixedUiElement } from "../Base/FixedUiElement"
import Link from "../Base/Link"
import { DropDown } from "../Input/DropDown"
import BaseUIElement from "../BaseUIElement"
import ValidatedTextField from "../Input/ValidatedTextField"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import Toggle, { ClickableToggle } from "../Input/Toggle"
import Table from "../Base/Table"
import LeftIndex from "../Base/LeftIndex"
import Toggleable, { Accordeon } from "../Base/Toggleable"
import TableOfContents from "../Base/TableOfContents"
import { LoginToggle } from "../Popup/LoginButton"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import Lazy from "../Base/Lazy"
import { Button } from "../Base/Button"
interface NoteProperties {
"id": number,
"url": string,
"date_created": string,
closed_at?: string,
"status": "open" | "closed",
"comments": {
date: string,
uid: number,
user: string,
text: string,
id: number
url: string
date_created: string
closed_at?: string
status: "open" | "closed"
comments: {
date: string
uid: number
user: string
text: string
html: string
}[]
}
interface NoteState {
props: NoteProperties,
theme: string,
intro: string,
dateStr: string,
status: "imported" | "already_mapped" | "invalid" | "closed" | "not_found" | "open" | "has_comments"
props: NoteProperties
theme: string
intro: string
dateStr: string
status:
| "imported"
| "already_mapped"
| "invalid"
| "closed"
| "not_found"
| "open"
| "has_comments"
}
class DownloadStatisticsButton extends SubtleButton {
constructor(states: NoteState[][]) {
super(Svg.statistics_svg(), "Download statistics");
super(Svg.statistics_svg(), "Download statistics")
this.onClick(() => {
const st: NoteState[] = [].concat(...states)
const fields = [
@ -61,26 +67,27 @@ class DownloadStatisticsButton extends SubtleButton {
"date_closed",
"days_open",
"intro",
"...comments"
"...comments",
]
const values: string[][] = st.map(note => {
return [note.props.id + "",
const values: string[][] = st.map((note) => {
return [
note.props.id + "",
note.status,
note.theme,
note.props.date_created?.substr(0, note.props.date_created.length - 3),
note.props.closed_at?.substr(0, note.props.closed_at.length - 3) ?? "",
JSON.stringify(note.intro),
...note.props.comments.map(c => JSON.stringify(c.user) + ": " + JSON.stringify(c.text))
...note.props.comments.map(
(c) => JSON.stringify(c.user) + ": " + JSON.stringify(c.text)
),
]
})
Utils.offerContentsAsDownloadableFile(
[fields, ...values].map(c => c.join(", ")).join("\n"),
[fields, ...values].map((c) => c.join(", ")).join("\n"),
"mapcomplete_import_notes_overview.csv",
{
mimetype: "text/csv"
mimetype: "text/csv",
}
)
})
@ -92,32 +99,32 @@ class MassAction extends Combine {
const textField = ValidatedTextField.ForType("text").ConstructInputElement()
const actions = new DropDown<{
predicate: (p: NoteProperties) => boolean,
predicate: (p: NoteProperties) => boolean
action: (p: NoteProperties) => Promise<void>
}>("On which notes should an action be performed?", [
{
value: undefined,
shown: <string | BaseUIElement>"Pick an option..."
shown: <string | BaseUIElement>"Pick an option...",
},
{
value: {
predicate: p => p.status === "open",
action: async p => {
predicate: (p) => p.status === "open",
action: async (p) => {
const txt = textField.GetValue().data
state.osmConnection.closeNote(p.id, txt)
}
},
},
shown: "Add comment to every open note and close all notes"
shown: "Add comment to every open note and close all notes",
},
{
value: {
predicate: p => p.status === "open",
action: async p => {
predicate: (p) => p.status === "open",
action: async (p) => {
const txt = textField.GetValue().data
state.osmConnection.addCommentToNote(p.id, txt)
}
},
},
shown: "Add comment to every open note"
shown: "Add comment to every open note",
},
/*
{
@ -131,25 +138,22 @@ class MassAction extends Combine {
},
shown:"On every open note, read the 'note='-tag and and this note as comment. (This action ignores the textfield)"
},//*/
])
const handledNotesCounter = new UIEventSource<number>(undefined)
const apply = new SubtleButton(Svg.checkmark_svg(), "Apply action")
.onClick(async () => {
const {predicate, action} = actions.GetValue().data
for (let i = 0; i < props.length; i++) {
handledNotesCounter.setData(i)
const prop = props[i]
if (!predicate(prop)) {
continue
}
await action(prop)
const apply = new SubtleButton(Svg.checkmark_svg(), "Apply action").onClick(async () => {
const { predicate, action } = actions.GetValue().data
for (let i = 0; i < props.length; i++) {
handledNotesCounter.setData(i)
const prop = props[i]
if (!predicate(prop)) {
continue
}
handledNotesCounter.setData(props.length)
})
await action(prop)
}
handledNotesCounter.setData(props.length)
})
super([
actions,
textField.SetClass("w-full border border-black"),
new Toggle(
@ -157,37 +161,57 @@ class MassAction extends Combine {
apply,
new Toggle(
new Loading(new VariableUiElement(handledNotesCounter.map(state => {
if (state === props.length) {
return "All done!"
}
return "Handling note " + (state + 1) + " out of " + props.length;
}))),
new Combine([Svg.checkmark_svg().SetClass("h-8"), "All done!"]).SetClass("thanks flex p-4"),
handledNotesCounter.map(s => s < props.length)
new Loading(
new VariableUiElement(
handledNotesCounter.map((state) => {
if (state === props.length) {
return "All done!"
}
return (
"Handling note " + (state + 1) + " out of " + props.length
)
})
)
),
new Combine([Svg.checkmark_svg().SetClass("h-8"), "All done!"]).SetClass(
"thanks flex p-4"
),
handledNotesCounter.map((s) => s < props.length)
),
handledNotesCounter.map(s => s === undefined)
)
handledNotesCounter.map((s) => s === undefined)
),
, new VariableUiElement(textField.GetValue().map(txt => "Type a text of at least 15 characters to apply the action. Currently, there are " + (txt?.length ?? 0) + " characters")).SetClass("alert"),
actions.GetValue().map(v => v !== undefined && textField.GetValue()?.data?.length > 15, [textField.GetValue()])
new VariableUiElement(
textField
.GetValue()
.map(
(txt) =>
"Type a text of at least 15 characters to apply the action. Currently, there are " +
(txt?.length ?? 0) +
" characters"
)
).SetClass("alert"),
actions
.GetValue()
.map(
(v) => v !== undefined && textField.GetValue()?.data?.length > 15,
[textField.GetValue()]
)
),
new Toggle(
new FixedUiElement("Testmode enable").SetClass("alert"), undefined,
new FixedUiElement("Testmode enable").SetClass("alert"),
undefined,
state.featureSwitchIsTesting
)
]);
),
])
}
}
class NoteTable extends Combine {
private static individualActions: [() => BaseUIElement, string][] = [
[Svg.not_found_svg, "This feature does not exist"],
[Svg.addSmall_svg, "imported"],
[Svg.duplicate_svg, "Already mapped"]
[Svg.duplicate_svg, "Already mapped"],
]
constructor(noteStates: NoteState[], state?: UserRelatedState) {
@ -195,18 +219,21 @@ class NoteTable extends Combine {
const table = new Table(
["id", "status", "last comment", "last modified by", "actions"],
noteStates.map(ns => NoteTable.noteField(ns, state)),
{sortable: true}
).SetClass("zebra-table link-underline");
noteStates.map((ns) => NoteTable.noteField(ns, state)),
{ sortable: true }
).SetClass("zebra-table link-underline")
super([
new Title("Mass apply an action on " + noteStates.length + " notes below"),
state !== undefined ? new MassAction(state, noteStates.map(ns => ns.props)).SetClass("block") : undefined,
state !== undefined
? new MassAction(
state,
noteStates.map((ns) => ns.props)
).SetClass("block")
: undefined,
table,
new Title("Example note", 4),
new FixedUiElement(typicalComment).SetClass("literal-code link-underline"),
])
this.SetClass("flex flex-col")
}
@ -214,9 +241,10 @@ class NoteTable extends Combine {
private static noteField(ns: NoteState, state: UserRelatedState) {
const link = new Link(
"" + ns.props.id,
"https://openstreetmap.org/note/" + ns.props.id, true
"https://openstreetmap.org/note/" + ns.props.id,
true
)
let last_comment = "";
let last_comment = ""
const last_comment_props = ns.props.comments[ns.props.comments.length - 1]
const before_last_comment = ns.props.comments[ns.props.comments.length - 2]
if (ns.props.comments.length > 1) {
@ -226,41 +254,56 @@ class NoteTable extends Combine {
}
}
const statusIcon = BatchView.icons[ns.status]().SetClass("h-4 w-4 shrink-0")
const togglestate = new UIEventSource(false);
const changed = new UIEventSource<string>(undefined);
const lazyButtons = new Lazy(( ) => new Combine(
this.individualActions.map(([img, text]) =>
img().onClick(async () => {
if (ns.props.status === "closed") {
await state.osmConnection.reopenNote(ns.props.id)
}
await state.osmConnection.closeNote(ns.props.id, text)
changed.setData(text)
}).SetClass("h-8 w-8"))
).SetClass("flex"));
const appliedButtons = new VariableUiElement(changed.map(currentState => currentState === undefined ? lazyButtons : currentState));
const buttons = Toggle.If(state?.osmConnection?.isLoggedIn,
() => new ClickableToggle(
appliedButtons,
new Button("edit...", () => {
console.log("Enabling...")
togglestate.setData(true);
}),
togglestate
));
return [link, new Combine([statusIcon, ns.status]).SetClass("flex"), last_comment,
new Link(last_comment_props.user, "https://www.openstreetmap.org/user/" + last_comment_props.user, true),
buttons
const togglestate = new UIEventSource(false)
const changed = new UIEventSource<string>(undefined)
const lazyButtons = new Lazy(() =>
new Combine(
this.individualActions.map(([img, text]) =>
img()
.onClick(async () => {
if (ns.props.status === "closed") {
await state.osmConnection.reopenNote(ns.props.id)
}
await state.osmConnection.closeNote(ns.props.id, text)
changed.setData(text)
})
.SetClass("h-8 w-8")
)
).SetClass("flex")
)
const appliedButtons = new VariableUiElement(
changed.map((currentState) => (currentState === undefined ? lazyButtons : currentState))
)
const buttons = Toggle.If(
state?.osmConnection?.isLoggedIn,
() =>
new ClickableToggle(
appliedButtons,
new Button("edit...", () => {
console.log("Enabling...")
togglestate.setData(true)
}),
togglestate
)
)
return [
link,
new Combine([statusIcon, ns.status]).SetClass("flex"),
last_comment,
new Link(
last_comment_props.user,
"https://www.openstreetmap.org/user/" + last_comment_props.user,
true
),
buttons,
]
}
}
class BatchView extends Toggleable {
public static icons = {
open: Svg.compass_svg,
has_comments: Svg.speech_bubble_svg,
@ -272,10 +315,9 @@ class BatchView extends Toggleable {
}
constructor(noteStates: NoteState[], state?: UserRelatedState) {
noteStates.sort((a, b) => a.props.id - b.props.id)
const {theme, intro, dateStr} = noteStates[0]
const { theme, intro, dateStr } = noteStates[0]
const statusHist = new Map<string, number>()
for (const noteState of noteStates) {
@ -284,109 +326,151 @@ class BatchView extends Toggleable {
statusHist.set(st, c + 1)
}
const unresolvedTotal = (statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0)
const badges: (BaseUIElement)[] = [
const unresolvedTotal =
(statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0)
const badges: BaseUIElement[] = [
new FixedUiElement(dateStr).SetClass("literal-code rounded-full"),
new FixedUiElement(noteStates.length + " total").SetClass("literal-code rounded-full ml-1 border-4 border-gray")
new FixedUiElement(noteStates.length + " total")
.SetClass("literal-code rounded-full ml-1 border-4 border-gray")
.onClick(() => filterOn.setData(undefined)),
unresolvedTotal === 0 ?
new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"])
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black") :
new FixedUiElement(Math.round(100 - 100 * unresolvedTotal / noteStates.length) + "%").SetClass("literal-code rounded-full ml-1")
unresolvedTotal === 0
? new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"]).SetClass(
"flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black"
)
: new FixedUiElement(
Math.round(100 - (100 * unresolvedTotal) / noteStates.length) + "%"
).SetClass("literal-code rounded-full ml-1"),
]
const filterOn = new UIEventSource<string>(undefined)
Object.keys(BatchView.icons).forEach(status => {
Object.keys(BatchView.icons).forEach((status) => {
const count = statusHist.get(status)
if (count === undefined) {
return undefined
}
const normal = new Combine([BatchView.icons[status]().SetClass("h-6 m-1"), count + " " + status])
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black")
const selected = new Combine([BatchView.icons[status]().SetClass("h-6 m-1"), count + " " + status])
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse")
const normal = new Combine([
BatchView.icons[status]().SetClass("h-6 m-1"),
count + " " + status,
]).SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black")
const selected = new Combine([
BatchView.icons[status]().SetClass("h-6 m-1"),
count + " " + status,
]).SetClass(
"flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse"
)
const toggle = new ClickableToggle(selected, normal, filterOn.sync(f => f === status, [], (selected, previous) => {
if (selected) {
return status;
}
if (previous === status) {
return undefined
}
return previous
})).ToggleOnClick()
const toggle = new ClickableToggle(
selected,
normal,
filterOn.sync(
(f) => f === status,
[],
(selected, previous) => {
if (selected) {
return status
}
if (previous === status) {
return undefined
}
return previous
}
)
).ToggleOnClick()
badges.push(toggle)
})
const fullTable = new NoteTable(noteStates, state);
const fullTable = new NoteTable(noteStates, state)
super(
new Combine([
new Title(theme + ": " + intro, 2),
new Combine(badges).SetClass("flex flex-wrap"),
]),
new VariableUiElement(filterOn.map(filter => {
if (filter === undefined) {
return fullTable
}
return new NoteTable(noteStates.filter(ns => ns.status === filter), state)
})),
new VariableUiElement(
filterOn.map((filter) => {
if (filter === undefined) {
return fullTable
}
return new NoteTable(
noteStates.filter((ns) => ns.status === filter),
state
)
})
),
{
closeOnClick: false
})
closeOnClick: false,
}
)
}
}
class ImportInspector extends VariableUiElement {
constructor(userDetails: { uid: number } | { display_name: string, search?: string }, state: UserRelatedState) {
let url;
constructor(
userDetails: { uid: number } | { display_name: string; search?: string },
state: UserRelatedState
) {
let url
if (userDetails["uid"] !== undefined) {
url = "https://api.openstreetmap.org/api/0.6/notes/search.json?user=" + userDetails["uid"] + "&closed=730&limit=10000&sort=created_at&q=%23import"
url =
"https://api.openstreetmap.org/api/0.6/notes/search.json?user=" +
userDetails["uid"] +
"&closed=730&limit=10000&sort=created_at&q=%23import"
} else {
url = "https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" +
encodeURIComponent(userDetails["display_name"]) + "&limit=10000&closed=730&sort=created_at&q=" + encodeURIComponent(userDetails["search"] ?? "#import")
url =
"https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" +
encodeURIComponent(userDetails["display_name"]) +
"&limit=10000&closed=730&sort=created_at&q=" +
encodeURIComponent(userDetails["search"] ?? "#import")
}
const notes: UIEventSource<
{ error: string } | { success: { features: { properties: NoteProperties }[] } }
> = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url))
super(
notes.map((notes) => {
if (notes === undefined) {
return new Loading("Loading notes which mention '#import'")
}
if (notes["error"] !== undefined) {
return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass(
"alert"
)
}
// We only care about the properties here
const props: NoteProperties[] = notes["success"].features.map((f) => f.properties)
const perBatch: NoteState[][] = Array.from(
ImportInspector.SplitNotesIntoBatches(props).values()
)
const els: Toggleable[] = perBatch.map(
(noteStates) => new BatchView(noteStates, state)
)
const notes: UIEventSource<{ error: string } | { success: { features: { properties: NoteProperties }[] } }> = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url))
super(notes.map(notes => {
if (notes === undefined) {
return new Loading("Loading notes which mention '#import'")
}
if (notes["error"] !== undefined) {
return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass("alert")
}
// We only care about the properties here
const props: NoteProperties[] = notes["success"].features.map(f => f.properties)
const perBatch: NoteState[][] = Array.from(ImportInspector.SplitNotesIntoBatches(props).values());
const els: Toggleable[] = perBatch.map(noteStates => new BatchView(noteStates, state))
const accordeon = new Accordeon(els)
let contents = [];
if (state?.osmConnection?.isLoggedIn?.data) {
contents =
[
const accordeon = new Accordeon(els)
let contents = []
if (state?.osmConnection?.isLoggedIn?.data) {
contents = [
new Title(Translations.t.importInspector.title, 1),
new SubtleButton(undefined, "Create a new batch of imports", {url: 'import_helper.html'})]
}
contents.push(accordeon)
const content = new Combine(contents)
return new LeftIndex(
[new TableOfContents(content, {noTopLevel: true, maxDepth: 1}).SetClass("subtle"),
new DownloadStatisticsButton(perBatch)
],
content
)
}));
new SubtleButton(undefined, "Create a new batch of imports", {
url: "import_helper.html",
}),
]
}
contents.push(accordeon)
const content = new Combine(contents)
return new LeftIndex(
[
new TableOfContents(content, { noTopLevel: true, maxDepth: 1 }).SetClass(
"subtle"
),
new DownloadStatisticsButton(perBatch),
],
content
)
})
)
}
/**
@ -397,7 +481,7 @@ class ImportInspector extends VariableUiElement {
const prefix = "https://mapcomplete.osm.be/"
for (const prop of props) {
const lines = prop.comments[0].text.split("\n")
const trigger = lines.findIndex(l => l.startsWith(prefix) && l.endsWith("#import"))
const trigger = lines.findIndex((l) => l.startsWith(prefix) && l.endsWith("#import"))
if (trigger < 0) {
continue
}
@ -409,16 +493,30 @@ class ImportInspector extends VariableUiElement {
if (!perBatch.has(key)) {
perBatch.set(key, [])
}
let status: "open" | "closed" | "imported" | "invalid" | "already_mapped" | "not_found" | "has_comments" = "open"
let status:
| "open"
| "closed"
| "imported"
| "invalid"
| "already_mapped"
| "not_found"
| "has_comments" = "open"
if (prop.closed_at !== undefined) {
const lastComment = prop.comments[prop.comments.length - 1].text.toLowerCase()
if (lastComment.indexOf("does not exist") >= 0) {
status = "not_found"
} else if (lastComment.indexOf("already mapped") >= 0) {
status = "already_mapped"
} else if (lastComment.indexOf("invalid") >= 0 || lastComment.indexOf("incorrecto") >= 0) {
} else if (
lastComment.indexOf("invalid") >= 0 ||
lastComment.indexOf("incorrecto") >= 0
) {
status = "invalid"
} else if (["imported", "erbij", "toegevoegd", "added"].some(keyword => lastComment.toLowerCase().indexOf(keyword) >= 0)) {
} else if (
["imported", "erbij", "toegevoegd", "added"].some(
(keyword) => lastComment.toLowerCase().indexOf(keyword) >= 0
)
) {
status = "imported"
} else {
status = "closed"
@ -435,28 +533,41 @@ class ImportInspector extends VariableUiElement {
status,
})
}
return perBatch;
return perBatch
}
}
class ImportViewerGui extends LoginToggle {
constructor() {
const state = new UserRelatedState(undefined)
const displayNameParam = QueryParameters.GetQueryParameter("user", "", "The username of the person whom you want to see the notes for");
const searchParam = QueryParameters.GetQueryParameter("search", "", "A text that should be included in the first comment of the note to be shown")
const displayNameParam = QueryParameters.GetQueryParameter(
"user",
"",
"The username of the person whom you want to see the notes for"
)
const searchParam = QueryParameters.GetQueryParameter(
"search",
"",
"A text that should be included in the first comment of the note to be shown"
)
super(
new VariableUiElement(state.osmConnection.userDetails.map(ud => {
const display_name = displayNameParam.data;
const search = searchParam.data;
if (display_name !== "" && search !== "") {
return new ImportInspector({display_name, search}, undefined);
}
return new ImportInspector(ud, state);
}, [displayNameParam, searchParam])),
"Login to inspect your import flows", state
new VariableUiElement(
state.osmConnection.userDetails.map(
(ud) => {
const display_name = displayNameParam.data
const search = searchParam.data
if (display_name !== "" && search !== "") {
return new ImportInspector({ display_name, search }, undefined)
}
return new ImportInspector(ud, state)
},
[displayNameParam, searchParam]
)
),
"Login to inspect your import flows",
state
)
}
}
new ImportViewerGui().AttachTo("main")
new ImportViewerGui().AttachTo("main")

View file

@ -1,45 +1,43 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import Title from "../Base/Title";
import {CreateNotes} from "./CreateNotes";
import {FixedUiElement} from "../Base/FixedUiElement";
import Combine from "../Base/Combine"
import { FlowStep } from "./FlowStep"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import Title from "../Base/Title"
import { CreateNotes } from "./CreateNotes"
import { FixedUiElement } from "../Base/FixedUiElement"
export default class Introdution extends Combine implements FlowStep<void> {
readonly IsValid: UIEventSource<boolean>;
readonly Value: UIEventSource<void>;
readonly IsValid: UIEventSource<boolean>
readonly Value: UIEventSource<void>
constructor() {
const example = CreateNotes.createNoteContentsUi({
properties:{
"some_key":"some_value",
"note":"a note in the original dataset"
const example = CreateNotes.createNoteContentsUi(
{
properties: {
some_key: "some_value",
note: "a note in the original dataset",
},
geometry: {
coordinates: [3.4, 51.2],
},
},
geometry:{
coordinates: [3.4,51.2]
{
wikilink:
"https://wiki.openstreetmap.org/wiki/Imports/<documentation of your import>",
intro: "There might be an XYZ here",
theme: "theme",
source: "source of the data",
}
}, {
wikilink: "https://wiki.openstreetmap.org/wiki/Imports/<documentation of your import>",
intro: "There might be an XYZ here",
theme: "theme",
source: "source of the data"
}).map(el => el === "" ? new FixedUiElement("").SetClass("block") : el)
).map((el) => (el === "" ? new FixedUiElement("").SetClass("block") : el))
super([
new Title(Translations.t.importHelper.introduction.title),
Translations.t.importHelper.introduction.description,
Translations.t.importHelper.introduction.importFormat,
new Combine(
[new Combine(
example
).SetClass("flex flex-col")
] ).SetClass("literal-code")
]);
new Combine([new Combine(example).SetClass("flex flex-col")]).SetClass("literal-code"),
])
this.SetClass("flex flex-col")
this. IsValid= new UIEventSource<boolean>(true);
this. Value = new UIEventSource<void>(undefined);
this.IsValid = new UIEventSource<boolean>(true)
this.Value = new UIEventSource<void>(undefined)
}
}
}

View file

@ -1,55 +1,74 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import Title from "../Base/Title";
import {VariableUiElement} from "../Base/VariableUIElement";
import {LoginToggle} from "../Popup/LoginButton";
import Img from "../Base/Img";
import Constants from "../../Models/Constants";
import Toggle from "../Input/Toggle";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import MoreScreen from "../BigComponents/MoreScreen";
import CheckBoxes from "../Input/Checkboxes";
import Combine from "../Base/Combine"
import { FlowStep } from "./FlowStep"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import Title from "../Base/Title"
import { VariableUiElement } from "../Base/VariableUIElement"
import { LoginToggle } from "../Popup/LoginButton"
import Img from "../Base/Img"
import Constants from "../../Models/Constants"
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: Store<boolean>;
readonly Value: Store<UserRelatedState>;
readonly IsValid: Store<boolean>
readonly Value: Store<UserRelatedState>
private static readonly whitelist = [15015689]
private static readonly whitelist = [15015689];
constructor(state: UserRelatedState) {
const t = Translations.t.importHelper.login
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)
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(
new VariableUiElement(state.osmConnection.userDetails.map(ud => {
if (ud === undefined) {
return undefined
}
return new Combine([
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()),
check
]);
})),
new VariableUiElement(
state.osmConnection.userDetails.map((ud) => {
if (ud === undefined) {
return undefined
}
return new Combine([
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()),
check,
])
})
),
t.loginRequired,
state
),
new Toggle(undefined,
new Combine(
[t.lockNotice.Subs(Constants.userJourney).SetClass("alert"),
MoreScreen.CreateProffessionalSerivesButton()])
, isValid)
new Toggle(
undefined,
new Combine([
t.lockNotice.Subs(Constants.userJourney).SetClass("alert"),
MoreScreen.CreateProffessionalSerivesButton(),
]),
isValid
),
])
this.Value = new UIEventSource<UserRelatedState>(state)
this.IsValid = isValid.map(isValid => isValid && check.GetValue().data.length > 0, [check.GetValue()]);
this.IsValid = isValid.map(
(isValid) => isValid && check.GetValue().data.length > 0,
[check.GetValue()]
)
}
}
}

View file

@ -1,82 +1,86 @@
import Combine from "../Base/Combine";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import {BBox} from "../../Logic/BBox";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import Translations from "../i18n/Translations";
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
import Constants from "../../Models/Constants";
import {DropDown} from "../Input/DropDown";
import {Utils} from "../../Utils";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import Loc from "../../Models/Loc";
import Minimap from "../Base/Minimap";
import Attribution from "../BigComponents/Attribution";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import Toggle from "../Input/Toggle";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {FlowStep} from "./FlowStep";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Title from "../Base/Title";
import CheckBoxes from "../Input/Checkboxes";
import {AllTagsPanel} from "../AllTagsPanel";
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch";
import Combine from "../Base/Combine"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { BBox } from "../../Logic/BBox"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Translations from "../i18n/Translations"
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
import Constants from "../../Models/Constants"
import { DropDown } from "../Input/DropDown"
import { Utils } from "../../Utils"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import BaseLayer from "../../Models/BaseLayer"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import Loc from "../../Models/Loc"
import Minimap from "../Base/Minimap"
import Attribution from "../BigComponents/Attribution"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import Toggle from "../Input/Toggle"
import { VariableUiElement } from "../Base/VariableUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import { FlowStep } from "./FlowStep"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Title from "../Base/Title"
import CheckBoxes from "../Input/Checkboxes"
import { AllTagsPanel } from "../AllTagsPanel"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
class PreviewPanel extends ScrollableFullScreen {
constructor(tags: UIEventSource<any>) {
super(
_ => new FixedUiElement("Element to import"),
_ => new Combine(["The tags are:",
new AllTagsPanel(tags)
]).SetClass("flex flex-col"),
(_) => new FixedUiElement("Element to import"),
(_) => new Combine(["The tags are:", new AllTagsPanel(tags)]).SetClass("flex flex-col"),
"element"
);
)
}
}
/**
* Shows the data to import on a map, asks for the correct layer to be selected
*/
export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[] }> {
public readonly IsValid: Store<boolean>;
public readonly Value: Store<{ bbox: BBox, layer: LayerConfig, features: any[] }>
export class MapPreview
extends Combine
implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: any[] }>
{
public readonly IsValid: Store<boolean>
public readonly Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[] }>
constructor(
state: UserRelatedState,
geojson: { features: { properties: any, geometry: { coordinates: [number, number] } }[] }) {
const t = Translations.t.importHelper.mapPreview;
geojson: { features: { properties: any; geometry: { coordinates: [number, number] } }[] }
) {
const t = Translations.t.importHelper.mapPreview
const propertyKeys = new Set<string>()
for (const f of geojson.features) {
Object.keys(f.properties).forEach(key => propertyKeys.add(key))
Object.keys(f.properties).forEach((key) => propertyKeys.add(key))
}
const availableLayers = AllKnownLayouts.AllPublicLayers().filter(l => l.name !== undefined && Constants.priviliged_layers.indexOf(l.id) < 0)
const layerPicker = new DropDown(t.selectLayer,
[{shown: t.selectLayer, value: undefined}].concat(availableLayers.map(l => ({
shown: l.name,
value: l
})))
const availableLayers = AllKnownLayouts.AllPublicLayers().filter(
(l) => l.name !== undefined && Constants.priviliged_layers.indexOf(l.id) < 0
)
const layerPicker = new DropDown(
t.selectLayer,
[{ shown: t.selectLayer, value: undefined }].concat(
availableLayers.map((l) => ({
shown: l.name,
value: l,
}))
)
)
let autodetected = new UIEventSource(false)
for (const layer of availableLayers) {
const mismatched = geojson.features.some(f =>
!layer.source.osmTags.matchesProperties(f.properties)
const mismatched = geojson.features.some(
(f) => !layer.source.osmTags.matchesProperties(f.properties)
)
if (!mismatched) {
console.log("Autodected layer", layer.id)
layerPicker.GetValue().setData(layer);
layerPicker.GetValue().addCallback(_ => autodetected.setData(false))
layerPicker.GetValue().setData(layer)
layerPicker.GetValue().addCallback((_) => autodetected.setData(false))
autodetected.setData(true)
break;
break
}
}
@ -86,65 +90,84 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer:
return copy
})
const matching: Store<{ properties: any, geometry: { coordinates: [number, number] } }[]> = layerPicker.GetValue().map((layer: LayerConfig) => {
if (layer === undefined) {
return [];
}
const matching: { properties: any, geometry: { coordinates: [number, number] } }[] = []
for (const feature of withId) {
if (layer.source.osmTags.matchesProperties(feature.properties)) {
matching.push(feature)
const matching: Store<{ properties: any; geometry: { coordinates: [number, number] } }[]> =
layerPicker.GetValue().map((layer: LayerConfig) => {
if (layer === undefined) {
return []
}
}
const matching: { properties: any; geometry: { coordinates: [number, number] } }[] =
[]
return matching
})
for (const feature of withId) {
if (layer.source.osmTags.matchesProperties(feature.properties)) {
matching.push(feature)
}
}
return matching
})
const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
const currentBounds = new UIEventSource<BBox>(undefined)
const map = Minimap.createMiniMap({
allowMoving: true,
location,
background,
bounds: currentBounds,
attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds)
attribution: new Attribution(
location,
state.osmConnection.userDetails,
undefined,
currentBounds
),
})
const layerControl = new BackgroundMapSwitch( {
backgroundLayer: background,
locationControl: location
},background)
const layerControl = new BackgroundMapSwitch(
{
backgroundLayer: background,
locationControl: location,
},
background
)
map.SetClass("w-full").SetStyle("height: 500px")
new ShowDataMultiLayer({
layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers()
.filter(l => l.source.geojsonSource === undefined)
.map(l => ({
layerDef: l,
isDisplayed: new UIEventSource<boolean>(true),
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
}))),
layers: new UIEventSource<FilteredLayer[]>(
AllKnownLayouts.AllPublicLayers()
.filter((l) => l.source.geojsonSource === undefined)
.map((l) => ({
layerDef: l,
isDisplayed: new UIEventSource<boolean>(true),
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined),
}))
),
zoomToFeatures: true,
features: StaticFeatureSource.fromDateless(matching.map(features => features.map(feature => ({feature})))),
features: StaticFeatureSource.fromDateless(
matching.map((features) => features.map((feature) => ({ feature })))
),
leafletMap: map.leafletMap,
popup: (tag) => new PreviewPanel(tag).SetClass("font-lg")
popup: (tag) => new PreviewPanel(tag).SetClass("font-lg"),
})
var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates]))))
var bbox = matching.map((feats) =>
BBox.bboxAroundAll(feats.map((f) => new BBox([f.geometry.coordinates])))
)
const mismatchIndicator = new VariableUiElement(
matching.map((matching) => {
if (matching === undefined) {
return undefined
}
const diff = geojson.features.length - matching.length
if (diff === 0) {
return undefined
}
const obligatory = layerPicker
.GetValue()
.data?.source?.osmTags?.asHumanString(false, false, {})
return t.mismatch.Subs({ count: diff, tags: obligatory }).SetClass("alert")
})
)
const mismatchIndicator = new VariableUiElement(matching.map(matching => {
if (matching === undefined) {
return undefined
}
const diff = geojson.features.length - matching.length;
if (diff === 0) {
return undefined
}
const obligatory = layerPicker.GetValue().data?.source?.osmTags?.asHumanString(false, false, {});
return t.mismatch.Subs({count: diff, tags: obligatory}).SetClass("alert")
}))
const confirm = new CheckBoxes([t.confirm]);
const confirm = new CheckBoxes([t.confirm])
super([
new Title(t.title, 1),
layerPicker,
@ -153,26 +176,30 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer:
mismatchIndicator,
map,
layerControl,
confirm
]);
confirm,
])
this.Value = bbox.map(bbox =>
({
this.Value = bbox.map(
(bbox) => ({
bbox,
features: geojson.features,
layer: layerPicker.GetValue().data
}), [layerPicker.GetValue()])
this.IsValid = matching.map(matching => {
if (matching === undefined) {
return false
}
if (confirm.GetValue().data.length !== 1) {
return false
}
const diff = geojson.features.length - matching.length;
return diff === 0;
}, [confirm.GetValue()])
layer: layerPicker.GetValue().data,
}),
[layerPicker.GetValue()]
)
this.IsValid = matching.map(
(matching) => {
if (matching === undefined) {
return false
}
if (confirm.GetValue().data.length !== 1) {
return false
}
const diff = geojson.features.length - matching.length
return diff === 0
},
[confirm.GetValue()]
)
}
}
}

View file

@ -1,47 +1,53 @@
import Combine from "../Base/Combine";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import Translations from "../i18n/Translations";
import {Utils} from "../../Utils";
import {FlowStep} from "./FlowStep";
import Title from "../Base/Title";
import BaseUIElement from "../BaseUIElement";
import Histogram from "../BigComponents/Histogram";
import Toggleable from "../Base/Toggleable";
import List from "../Base/List";
import CheckBoxes from "../Input/Checkboxes";
import Combine from "../Base/Combine"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import { FlowStep } from "./FlowStep"
import Title from "../Base/Title"
import BaseUIElement from "../BaseUIElement"
import Histogram from "../BigComponents/Histogram"
import Toggleable from "../Base/Toggleable"
import List from "../Base/List"
import CheckBoxes from "../Input/Checkboxes"
/**
* Shows the attributes by value, requests to check them of
*/
export class PreviewAttributesPanel extends Combine implements FlowStep<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> {
public readonly IsValid: Store<boolean>;
public readonly Value: Store<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }>
export class PreviewAttributesPanel
extends Combine
implements
FlowStep<{ features: { properties: any; geometry: { coordinates: [number, number] } }[] }>
{
public readonly IsValid: Store<boolean>
public readonly Value: Store<{
features: { properties: any; geometry: { coordinates: [number, number] } }[]
}>
constructor(
state: UserRelatedState,
geojson: { features: { properties: any, geometry: { coordinates: [number, number] } }[] }) {
const t = Translations.t.importHelper.previewAttributes;
geojson: { features: { properties: any; geometry: { coordinates: [number, number] } }[] }
) {
const t = Translations.t.importHelper.previewAttributes
const propertyKeys = new Set<string>()
for (const f of geojson.features) {
Object.keys(f.properties).forEach(key => propertyKeys.add(key))
Object.keys(f.properties).forEach((key) => propertyKeys.add(key))
}
const attributeOverview: BaseUIElement[] = []
const n = geojson.features.length;
const n = geojson.features.length
for (const key of Array.from(propertyKeys)) {
const values = Utils.NoNull(geojson.features.map(f => f.properties[key]))
const allSame = !values.some(v => v !== values[0])
const values = Utils.NoNull(geojson.features.map((f) => f.properties[key]))
const allSame = !values.some((v) => v !== values[0])
let countSummary: BaseUIElement
if (values.length === n) {
countSummary = t.allAttributesSame
} else {
countSummary = t.someHaveSame.Subs({
count: values.length,
percentage: Math.floor(100 * values.length / n)
percentage: Math.floor((100 * values.length) / n),
})
}
if (allSame) {
@ -54,25 +60,16 @@ export class PreviewAttributesPanel extends Combine implements FlowStep<{ featur
if (uniqueCount !== values.length && uniqueCount < 15) {
attributeOverview.push()
// There are some overlapping values: histogram time!
let hist: BaseUIElement =
new Combine([
countSummary,
new Histogram(
new UIEventSource<string[]>(values),
"Value",
"Occurence",
{
sortMode: "count-rev"
})
]).SetClass("flex flex-col")
let hist: BaseUIElement = new Combine([
countSummary,
new Histogram(new UIEventSource<string[]>(values), "Value", "Occurence", {
sortMode: "count-rev",
}),
]).SetClass("flex flex-col")
const title = new Title(key + "=*")
if (uniqueCount > 15) {
hist = new Toggleable(title,
hist.SetClass("block")
).Collapse()
hist = new Toggleable(title, hist.SetClass("block")).Collapse()
} else {
attributeOverview.push(title)
}
@ -82,27 +79,23 @@ export class PreviewAttributesPanel extends Combine implements FlowStep<{ featur
}
// All values are different or too much unique values, we add a boring (but collapsable) list
attributeOverview.push(new Toggleable(
new Title(key + "=*"),
new Combine([
countSummary,
new List(values)
])
))
attributeOverview.push(
new Toggleable(new Title(key + "=*"), new Combine([countSummary, new List(values)]))
)
}
const confirm = new CheckBoxes([t.inspectLooksCorrect])
super([
new Title(t.inspectDataTitle.Subs({count: geojson.features.length})),
new Title(t.inspectDataTitle.Subs({ count: geojson.features.length })),
"Extra remark: An attribute with 'source' or 'src' will be added as 'source' into the map pin; an attribute 'note' will be added into the map pin as well. These values won't be imported",
...attributeOverview,
confirm
]);
this.Value = new UIEventSource<{ features: { properties: any; geometry: { coordinates: [number, number] } }[] }>(geojson)
this.IsValid = confirm.GetValue().map(selected => selected.length == 1)
confirm,
])
this.Value = new UIEventSource<{
features: { properties: any; geometry: { coordinates: [number, number] } }[]
}>(geojson)
this.IsValid = confirm.GetValue().map((selected) => selected.length == 1)
}
}
}

View file

@ -1,33 +1,33 @@
import Combine from "../Base/Combine";
import {Store, Stores} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import {SubtleButton} from "../Base/SubtleButton";
import {VariableUiElement} from "../Base/VariableUIElement";
import Title from "../Base/Title";
import InputElementMap from "../Input/InputElementMap";
import BaseUIElement from "../BaseUIElement";
import FileSelectorButton from "../Input/FileSelectorButton";
import {FlowStep} from "./FlowStep";
import {parse} from "papaparse";
import {FixedUiElement} from "../Base/FixedUiElement";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import Combine from "../Base/Combine"
import { Store, Stores } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { SubtleButton } from "../Base/SubtleButton"
import { VariableUiElement } from "../Base/VariableUIElement"
import Title from "../Base/Title"
import InputElementMap from "../Input/InputElementMap"
import BaseUIElement from "../BaseUIElement"
import FileSelectorButton from "../Input/FileSelectorButton"
import { FlowStep } from "./FlowStep"
import { parse } from "papaparse"
import { FixedUiElement } from "../Base/FixedUiElement"
import { TagUtils } from "../../Logic/Tags/TagUtils"
class FileSelector extends InputElementMap<FileList, { name: string, contents: Promise<string> }> {
class FileSelector extends InputElementMap<FileList, { name: string; contents: Promise<string> }> {
constructor(label: BaseUIElement) {
super(
new FileSelectorButton(label, {allowMultiple: false, acceptType: "*"}),
new FileSelectorButton(label, { allowMultiple: false, acceptType: "*" }),
(x0, x1) => {
// Total hack: x1 is undefined is the backvalue - we effectively make this a one-way-story
return x1 === undefined || x0 === x1;
return x1 === undefined || x0 === x1
},
filelist => {
(filelist) => {
if (filelist === undefined) {
return undefined
}
const file = filelist.item(0)
return {name: file.name, contents: file.text()}
return { name: file.name, contents: file.text() }
},
_ => undefined
(_) => undefined
)
}
}
@ -35,149 +35,153 @@ 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<{features: any[]}> {
export class RequestFile extends Combine implements FlowStep<{ features: any[] }> {
public readonly IsValid: Store<boolean>
/**
* The loaded GeoJSON
*/
public readonly Value: Store<{features: any[]}>
public readonly Value: Store<{ features: any[] }>
constructor() {
const t = Translations.t.importHelper.selectFile;
const t = Translations.t.importHelper.selectFile
const csvSelector = new FileSelector(new SubtleButton(undefined, t.description))
const loadedFiles = new VariableUiElement(csvSelector.GetValue().map(file => {
if (file === undefined) {
return t.noFilesLoaded.SetClass("alert")
}
return t.loadedFilesAre.Subs({file: file.name}).SetClass("thanks")
}))
const loadedFiles = new VariableUiElement(
csvSelector.GetValue().map((file) => {
if (file === undefined) {
return t.noFilesLoaded.SetClass("alert")
}
return t.loadedFilesAre.Subs({ file: file.name }).SetClass("thanks")
})
)
const text = Stores.flatten(
csvSelector.GetValue().map(v => {
csvSelector.GetValue().map((v) => {
if (v === undefined) {
return undefined
}
return Stores.FromPromise(v.contents)
}))
})
)
const asGeoJson: Store<any | { error: string | BaseUIElement }> = text.map((src: string) => {
if (src === undefined) {
return undefined
}
try {
const parsed = JSON.parse(src)
if (parsed["type"] !== "FeatureCollection") {
return {error: t.errNotFeatureCollection}
const asGeoJson: Store<any | { error: string | BaseUIElement }> = text.map(
(src: string) => {
if (src === undefined) {
return undefined
}
if (parsed.features.some(f => f.geometry.type != "Point")) {
return {error: t.errPointsOnly}
}
parsed.features.forEach(f => {
const props = f.properties
for (const key in props) {
if(props[key] === undefined || props[key] === null || props[key] === ""){
delete props[key]
}
if(!TagUtils.isValidKey(key)){
return {error: "Probably an invalid key: "+key}
try {
const parsed = JSON.parse(src)
if (parsed["type"] !== "FeatureCollection") {
return { error: t.errNotFeatureCollection }
}
if (parsed.features.some((f) => f.geometry.type != "Point")) {
return { error: t.errPointsOnly }
}
})
return parsed;
} catch (e) {
// Loading as CSV
var lines: string[][] = <any>parse(src).data;
const header = lines[0]
lines.splice(0, 1)
if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) {
return {error: t.errNoLatOrLon}
}
if (header.some(h => h.trim() == "")) {
return {error: t.errNoName}
}
if (new Set(header).size !== header.length) {
return {error: t.errDuplicate}
}
const features = []
for (let i = 0; i < lines.length; i++) {
const attrs = lines[i];
if (attrs.length == 0 || (attrs.length == 1 && attrs[0] == "")) {
// empty line
continue
parsed.features.forEach((f) => {
const props = f.properties
for (const key in props) {
if (
props[key] === undefined ||
props[key] === null ||
props[key] === ""
) {
delete props[key]
}
if (!TagUtils.isValidKey(key)) {
return { error: "Probably an invalid key: " + key }
}
}
})
return parsed
} catch (e) {
// Loading as CSV
var lines: string[][] = <any>parse(src).data
const header = lines[0]
lines.splice(0, 1)
if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) {
return { error: t.errNoLatOrLon }
}
const properties = {}
for (let i = 0; i < header.length; i++) {
const v = attrs[i]
if (v === undefined || v === "") {
if (header.some((h) => h.trim() == "")) {
return { error: t.errNoName }
}
if (new Set(header).size !== header.length) {
return { error: t.errDuplicate }
}
const features = []
for (let i = 0; i < lines.length; i++) {
const attrs = lines[i]
if (attrs.length == 0 || (attrs.length == 1 && attrs[0] == "")) {
// empty line
continue
}
properties[header[i]] = v;
}
const coordinates = [Number(properties["lon"]), Number(properties["lat"])]
delete properties["lat"]
delete properties["lon"]
if (coordinates.some(isNaN)) {
return {error: "A coordinate could not be parsed for line " + (i + 2)}
}
const f = {
type: "Feature",
properties,
geometry: {
type: "Point",
coordinates
const properties = {}
for (let i = 0; i < header.length; i++) {
const v = attrs[i]
if (v === undefined || v === "") {
continue
}
properties[header[i]] = v
}
};
features.push(f)
}
const coordinates = [Number(properties["lon"]), Number(properties["lat"])]
delete properties["lat"]
delete properties["lon"]
if (coordinates.some(isNaN)) {
return { error: "A coordinate could not be parsed for line " + (i + 2) }
}
const f = {
type: "Feature",
properties,
geometry: {
type: "Point",
coordinates,
},
}
features.push(f)
}
return {
type: "FeatureCollection",
features
return {
type: "FeatureCollection",
features,
}
}
}
})
)
const errorIndicator = new VariableUiElement(asGeoJson.map(v => {
if (v === undefined) {
return undefined;
}
if (v?.error === undefined) {
return undefined;
}
let err: BaseUIElement;
if(typeof v.error === "string"){
err = new FixedUiElement(v.error)
}else if(v.error.Clone !== undefined){
err = v.error.Clone()
}else{
err = v.error
}
return err.SetClass("alert");
}))
const errorIndicator = new VariableUiElement(
asGeoJson.map((v) => {
if (v === undefined) {
return undefined
}
if (v?.error === undefined) {
return undefined
}
let err: BaseUIElement
if (typeof v.error === "string") {
err = new FixedUiElement(v.error)
} else if (v.error.Clone !== undefined) {
err = v.error.Clone()
} else {
err = v.error
}
return err.SetClass("alert")
})
)
super([
new Title(t.title, 1),
t.fileFormatDescription,
t.fileFormatDescriptionCsv,
t.fileFormatDescriptionGeoJson,
csvSelector,
loadedFiles,
errorIndicator
]);
errorIndicator,
])
this.SetClass("flex flex-col wi")
this.IsValid = asGeoJson.map(geojson => geojson !== undefined && geojson["error"] === undefined)
this.IsValid = asGeoJson.map(
(geojson) => geojson !== undefined && geojson["error"] === undefined
)
this.Value = asGeoJson
}
}
}

View file

@ -1,156 +1,183 @@
import {FlowStep} from "./FlowStep";
import Combine from "../Base/Combine";
import {Store} 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 Toggleable from "../Base/Toggleable";
import {BBox} from "../../Logic/BBox";
import BaseUIElement from "../BaseUIElement";
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
import List from "../Base/List";
import Translations from "../i18n/Translations";
export default class SelectTheme extends Combine implements FlowStep<{
features: any[],
theme: string,
layer: LayerConfig,
bbox: BBox,
}> {
import { FlowStep } from "./FlowStep"
import Combine from "../Base/Combine"
import { Store } 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 Toggleable from "../Base/Toggleable"
import { BBox } from "../../Logic/BBox"
import BaseUIElement from "../BaseUIElement"
import PresetConfig from "../../Models/ThemeConfig/PresetConfig"
import List from "../Base/List"
import Translations from "../i18n/Translations"
export default class SelectTheme
extends Combine
implements
FlowStep<{
features: any[]
theme: string
layer: LayerConfig
bbox: BBox
}>
{
public readonly Value: Store<{
features: any[],
theme: string,
layer: LayerConfig,
bbox: BBox,
}>;
public readonly IsValid: Store<boolean>;
features: any[]
theme: string
layer: LayerConfig
bbox: BBox
}>
public readonly IsValid: Store<boolean>
constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) {
constructor(params: { features: any[]; layer: LayerConfig; bbox: BBox }) {
const t = Translations.t.importHelper.selectTheme
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))
.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
selectFirstAsDefault: false,
})
const applicablePresets = themeRadios.GetValue().map(theme => {
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)
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 => {
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)))
return params.features.filter(
(feat) =>
!presets.some((preset) =>
new And(preset.tags).matchesProperties(feat.properties)
)
)
})
super([
new Title(t.title),
t.intro,
t.intro,
themeRadios,
new VariableUiElement(applicablePresets.map(applicablePresets => {
if (themeRadios.GetValue().data === undefined) {
return undefined
}
if (applicablePresets === undefined || applicablePresets.length === 0) {
return t.noMatchingPresets.SetClass("alert")
}
}, [themeRadios.GetValue()])),
new VariableUiElement(
applicablePresets.map(
(applicablePresets) => {
if (themeRadios.GetValue().data === undefined) {
return undefined
}
if (applicablePresets === undefined || applicablePresets.length === 0) {
return t.noMatchingPresets.SetClass("alert")
}
},
[themeRadios.GetValue()]
)
),
new VariableUiElement(nonMatchedElements.map(unmatched => SelectTheme.nonMatchedElementsPanel(unmatched, applicablePresets.data), [applicablePresets]))
]);
new VariableUiElement(
nonMatchedElements.map(
(unmatched) =>
SelectTheme.nonMatchedElementsPanel(unmatched, applicablePresets.data),
[applicablePresets]
)
),
])
this.SetClass("flex flex-col")
this.Value = themeRadios.GetValue().map(theme => ({
this.Value = themeRadios.GetValue().map((theme) => ({
features: params.features,
layer: params.layer,
bbox: params.bbox,
theme
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;
}
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])
return true
},
[applicablePresets]
)
}
private static nonMatchedElementsPanel(unmatched: any[], applicablePresets: PresetConfig[]): BaseUIElement {
private static nonMatchedElementsPanel(
unmatched: any[],
applicablePresets: PresetConfig[]
): BaseUIElement {
if (unmatched === undefined || unmatched.length === 0) {
return
}
const t = Translations.t.importHelper.selectTheme
const applicablePresetsOverview = applicablePresets.map(preset =>
t.needsTags.Subs(
{title: preset.title,
tags:preset.tags.map(t => t.asHumanString()).join(" & ") })
const t = Translations.t.importHelper.selectTheme
const applicablePresetsOverview = applicablePresets.map((preset) =>
t.needsTags
.Subs({
title: preset.title,
tags: preset.tags.map((t) => t.asHumanString()).join(" & "),
})
.SetClass("thanks")
);
)
const unmatchedPanels: BaseUIElement[] = []
for (const feat of unmatched) {
const parts: BaseUIElement[] = []
parts.push(new Combine(Object.keys(feat.properties).map(k =>
k+"="+feat.properties[k]
)).SetClass("flex flex-col"))
parts.push(
new Combine(
Object.keys(feat.properties).map((k) => k + "=" + feat.properties[k])
).SetClass("flex flex-col")
)
for (const preset of applicablePresets) {
const tags = new And(preset.tags).asChange({})
const missing = []
for (const {k, v} of tags) {
for (const { k, v } of tags) {
if (preset[k] === undefined) {
missing.push(t.missing.Subs({k,v}))
missing.push(t.missing.Subs({ k, v }))
} else if (feat.properties[k] !== v) {
missing.push(t.misMatch.Subs({k, v, properties: feat.properties}))
missing.push(t.misMatch.Subs({ k, v, properties: feat.properties }))
}
}
if (missing.length > 0) {
parts.push(
new Combine([
t.notApplicable.Subs(preset),
new List(missing)
]).SetClass("flex flex-col alert")
new Combine([t.notApplicable.Subs(preset), new List(missing)]).SetClass(
"flex flex-col alert"
)
)
}
}
unmatchedPanels.push(new Combine(parts).SetClass("flex flex-col"))
@ -159,11 +186,7 @@ export default class SelectTheme extends Combine implements FlowStep<{
return new Combine([
t.displayNonMatchingCount.Subs(unmatched).SetClass("alert"),
...applicablePresetsOverview,
new Toggleable(new Title(t.unmatchedTitle),
new Combine(unmatchedPanels))
new Toggleable(new Title(t.unmatchedTitle), new Combine(unmatchedPanels)),
]).SetClass("flex flex-col")
}
}
}