forked from MapComplete/MapComplete
Reformat all files with prettier
This commit is contained in:
parent
e22d189376
commit
b541d3eab4
382 changed files with 50893 additions and 35566 deletions
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue