forked from MapComplete/MapComplete
Merge master
This commit is contained in:
commit
49d99930e8
18 changed files with 413 additions and 209 deletions
|
@ -4,8 +4,12 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
|
||||
import Title from "../Base/Title";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export class AskMetadata extends Combine implements FlowStep<{
|
||||
features: any[],
|
||||
|
@ -25,7 +29,7 @@ export class AskMetadata extends Combine implements FlowStep<{
|
|||
public readonly IsValid: UIEventSource<boolean>;
|
||||
|
||||
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%"
|
||||
|
@ -42,28 +46,39 @@ export class AskMetadata extends Combine implements FlowStep<{
|
|||
})
|
||||
|
||||
super([
|
||||
new Title("Set metadata"),
|
||||
"Before adding " + params.features.length + " notes, please provide some extra information.",
|
||||
"Please, write an introduction for someone who sees the note",
|
||||
new Title(t.title),
|
||||
t.intro.Subs({count: params.features.length}),
|
||||
t.giveDescription,
|
||||
introduction.SetClass("w-full border border-black"),
|
||||
"What is the source of this data? If 'source' is set in the feature, this value will be ignored",
|
||||
source.SetClass("w-full border border-black"),
|
||||
"On what wikipage can one find more information about this import?",
|
||||
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 new FixedUiElement("Expected a link to wiki.openstreetmap.org").SetClass("alert");
|
||||
return t.shouldBeOsmWikilink.SetClass("alert");
|
||||
}
|
||||
|
||||
if(url.pathname.toLowerCase() === "/wiki/main_page"){
|
||||
return new FixedUiElement("Nope, the home page isn't allowed either. Enter the URL of a proper wikipage documenting your import").SetClass("alert");
|
||||
return t.shouldNotBeHomepage.SetClass("alert");
|
||||
}
|
||||
}catch(e){
|
||||
return new FixedUiElement("Not a valid URL").SetClass("alert")
|
||||
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"
|
||||
})
|
||||
})
|
||||
]);
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import {FixedUiElement} from "../Base/FixedUiElement";
|
|||
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";
|
||||
|
||||
/**
|
||||
* Filters out points for which the import-note already exists, to prevent duplicates
|
||||
|
@ -28,11 +29,11 @@ import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
|||
export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> {
|
||||
|
||||
public IsValid: UIEventSource<boolean>
|
||||
public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[] , theme: string}>
|
||||
public Value: UIEventSource<{ 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]
|
||||
if (layerConfig === undefined) {
|
||||
console.error("WEIRD: layer not found in the builtin layer overview")
|
||||
|
@ -45,7 +46,7 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
|||
layerDef: importLayer
|
||||
}
|
||||
const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
|
||||
|
||||
|
||||
allNotesWithinBbox.features.map(f => MetaTagging.addMetatags(
|
||||
f,
|
||||
{
|
||||
|
@ -63,7 +64,6 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
|||
)
|
||||
)
|
||||
const alreadyOpenImportNotes = new FilteringFeatureSource(state, undefined, allNotesWithinBbox)
|
||||
alreadyOpenImportNotes.features.addCallbackD(features => console.log("Loaded and filtered features are", features))
|
||||
const map = Minimap.createMiniMap()
|
||||
map.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
|
@ -99,43 +99,46 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
|
|||
})
|
||||
|
||||
super([
|
||||
new Title("Compare with already existing 'to-import'-notes"),
|
||||
new Title(t.titleLong),
|
||||
new VariableUiElement(
|
||||
alreadyOpenImportNotes.features.map(notesWithImport => {
|
||||
if(allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined){
|
||||
return new FixedUiElement("Loading notes failed: "+allNotesWithinBbox.state.data["error"] )
|
||||
if (allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined) {
|
||||
t.loadingFailed.Subs(allNotesWithinBbox.state.data)
|
||||
}
|
||||
if (allNotesWithinBbox.features.data === undefined || allNotesWithinBbox.features.data.length === 0) {
|
||||
return new Loading("Fetching notes from OSM")
|
||||
return new Loading(t.loading)
|
||||
}
|
||||
if (notesWithImport.length === 0) {
|
||||
return new FixedUiElement("No previous import notes found").SetClass("thanks")
|
||||
return t.noPreviousNotesFound.SetClass("thanks")
|
||||
}
|
||||
return new Combine([
|
||||
"The red elements on the next map are all the data points from your dataset. There are <b>"+params.features.length+"</b> elements in your dataset.",
|
||||
t.mapExplanation.Subs(params.features),
|
||||
map,
|
||||
|
||||
new VariableUiElement( partitionedImportPoints.map(({noNearby, hasNearby}) => {
|
||||
|
||||
if(noNearby.length === 0){
|
||||
// Nothing can be imported
|
||||
return new FixedUiElement("All of the proposed points have (or had) an import note already").SetClass("alert w-full block").SetStyle("padding: 0.5rem")
|
||||
}
|
||||
|
||||
if(hasNearby.length === 0){
|
||||
// All points can be imported
|
||||
return new FixedUiElement("All of the proposed points have don't have a previous import note nearby").SetClass("thanks w-full block").SetStyle("padding: 0.5rem")
|
||||
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
new FixedUiElement(hasNearby.length+" points do have an already existing import note within "+maxDistance.data+" meter.").SetClass("alert"),
|
||||
"These data points will <i>not</i> be imported and are shown as red dots on the map below",
|
||||
comparisonMap.SetClass("w-full")
|
||||
]).SetClass("w-full")
|
||||
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 (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])
|
||||
|
|
|
@ -2,45 +2,30 @@ import Combine from "../Base/Combine";
|
|||
import {FlowStep} from "./FlowStep";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Link from "../Base/Link";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import CheckBoxes from "../Input/Checkboxes";
|
||||
import Title from "../Base/Title";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import {Utils} from "../../Utils";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export class ConfirmProcess extends Combine implements FlowStep<{ features: any[], theme: string }> {
|
||||
|
||||
public IsValid: UIEventSource<boolean>
|
||||
public Value: UIEventSource<{ features: any[],theme: string }>
|
||||
|
||||
constructor(v: { features: any[], theme: string }) {
|
||||
public Value: UIEventSource<{ features: any[], theme: string }>
|
||||
|
||||
constructor(v: { features: any[], theme: string }) {
|
||||
const t = Translations.t.importHelper.confirmProcess;
|
||||
const toConfirm = [
|
||||
new Combine(["I have read the ", new Link("import guidelines on the OSM wiki", "https://wiki.openstreetmap.org/wiki/Import_guidelines", true)]),
|
||||
new FixedUiElement("I did contact the (local) community about this import"),
|
||||
new FixedUiElement("The license of the data to import allows it to be imported into OSM. They are allowed to be redistributed commercially, with only minimal attribution"),
|
||||
new FixedUiElement("The process is documented on the OSM-wiki (you'll need this link later)")
|
||||
new Link(t.readImportGuidelines, "https://wiki.openstreetmap.org/wiki/Import_guidelines", true),
|
||||
t.contactedCommunity,
|
||||
t.licenseIsCompatible,
|
||||
t.wikipageIsMade
|
||||
];
|
||||
|
||||
const licenseClear = new CheckBoxes(toConfirm)
|
||||
super([
|
||||
new Title("Did you go through the import process?"),
|
||||
licenseClear,
|
||||
new FixedUiElement("Alternatively, you can download the dataset to import directly"),
|
||||
new SubtleButton(Svg.download_svg(), "Download geojson").OnClickWithLoading("Preparing your download",
|
||||
async ( ) => {
|
||||
const geojson = {
|
||||
type:"FeatureCollection",
|
||||
features: v.features
|
||||
}
|
||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "prepared_import_"+v.theme+".geojson",{
|
||||
mimetype: "application/vnd.geo+json"
|
||||
})
|
||||
})
|
||||
new Title(t.titleLong),
|
||||
new CheckBoxes(toConfirm),
|
||||
]);
|
||||
this.SetClass("link-underline")
|
||||
this.IsValid = licenseClear.GetValue().map(selected => toConfirm.length == selected.length)
|
||||
this.IsValid = new CheckBoxes(toConfirm).GetValue().map(selected => toConfirm.length == selected.length)
|
||||
this.Value = new UIEventSource<{ features: any[], theme: string }>(v)
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import Constants from "../../Models/Constants";
|
||||
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {FlowStep} from "./FlowStep";
|
||||
import Loading from "../Base/Loading";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
|
@ -28,6 +27,7 @@ import * as import_candidate from "../../assets/layers/import_candidate/import_c
|
|||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
import {ImportUtils} from "./ImportUtils";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
/**
|
||||
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
||||
|
@ -190,6 +190,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
features: toImportWithNearby
|
||||
})
|
||||
|
||||
const t = Translations.t.importHelper.conflationChecker
|
||||
|
||||
const conflationMaps = new Combine([
|
||||
new VariableUiElement(
|
||||
|
@ -197,7 +198,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
if (geojson === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => {
|
||||
return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick(() => {
|
||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
|
||||
mimetype: "application/json+geo"
|
||||
})
|
||||
|
@ -208,43 +209,53 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
|
|||
return undefined;
|
||||
}
|
||||
if (age < 0) {
|
||||
return new FixedUiElement("Cache was expired")
|
||||
return t.cacheExpired
|
||||
}
|
||||
return new FixedUiElement("Loaded data is from the cache and is " + Utils.toHumanTime(age) + " old")
|
||||
return t.loadedDataAge.Subs({age: Utils.toHumanTime(age)})
|
||||
})),
|
||||
|
||||
new Title("Live data on OSM"),
|
||||
"The "+toImport.features.length+" red elements on the following map are all your import candidates.",
|
||||
new VariableUiElement(geojson.map(geojson => new FixedUiElement((geojson?.features?.length ?? "No") + " elements are loaded from OpenStreetMap which match the layer "+layer.id+". Zooming in might be needed to show them"))),
|
||||
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}),
|
||||
|
||||
])
|
||||
})),
|
||||
osmLiveData,
|
||||
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => "" + l.zoom))]).SetClass("flex"),
|
||||
|
||||
new Title("Nearby features"),
|
||||
new Combine(["The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
|
||||
new VariableUiElement(osmLiveData.location.map(location => {
|
||||
return t.zoomIn.Subs({needed:zoomLevel, current: location.zoom })
|
||||
} )),
|
||||
new Title(t.titleNearby),
|
||||
new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"),
|
||||
new VariableUiElement(toImportWithNearby.features.map(feats =>
|
||||
new FixedUiElement("The "+ feats.length +" red elements on the following map will <b>not</b> be imported!").SetClass("alert"))),
|
||||
"Set the range to 0 or 1 if you want to import them all",
|
||||
t.nearbyWarn.Subs({count: feats.length}).SetClass("alert"))),
|
||||
,t.setRangeToZero,
|
||||
matchedFeaturesMap]).SetClass("flex flex-col")
|
||||
|
||||
super([
|
||||
new Title("Comparison with existing data"),
|
||||
new Title(t.title),
|
||||
new VariableUiElement(overpassStatus.map(d => {
|
||||
if (d === "idle") {
|
||||
return new Loading("Checking local storage...")
|
||||
}
|
||||
if (d["error"] !== undefined) {
|
||||
return new FixedUiElement("Could not load latest data from overpass: " + d["error"]).SetClass("alert")
|
||||
return new Loading(t.states.idle)
|
||||
}
|
||||
if (d === "running") {
|
||||
return new Loading("Querying overpass...")
|
||||
return new Loading(t.states.running)
|
||||
}
|
||||
if (d["error"] !== undefined) {
|
||||
return t.states.error.Subs(d).SetClass("alert")
|
||||
}
|
||||
|
||||
if (d === "cached") {
|
||||
return conflationMaps
|
||||
}
|
||||
if (d === "success") {
|
||||
return conflationMaps
|
||||
}
|
||||
return new FixedUiElement("Unexpected state " + d).SetClass("alert")
|
||||
return t.states.unexpected.Subs({state: d}).SetClass("alert")
|
||||
}))
|
||||
|
||||
])
|
||||
|
|
|
@ -8,47 +8,58 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export class CreateNotes extends Combine {
|
||||
|
||||
public static createNoteContents(feature: {properties: any, geometry: {coordinates: [number,number]}},
|
||||
options: {wikilink: string; intro: string; source: string, theme: string }
|
||||
): string[]{
|
||||
const src = feature.properties["source"] ?? feature.properties["src"] ?? options.source
|
||||
delete feature.properties["source"]
|
||||
delete feature.properties["src"]
|
||||
let extraNote = ""
|
||||
if (feature.properties["note"]) {
|
||||
extraNote = feature.properties["note"] + "\n"
|
||||
delete feature.properties["note"]
|
||||
}
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
if (feature.properties[key] === "") {
|
||||
continue
|
||||
}
|
||||
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]
|
||||
const note = Translations.t.importHelper.noteParts
|
||||
return [
|
||||
options.intro,
|
||||
extraNote,
|
||||
note.datasource.Subs({source: src}).txt,
|
||||
note.wikilink.Subs(options).txt,
|
||||
'',
|
||||
note.importEasily.txt,
|
||||
`https://mapcomplete.osm.be/${options.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
|
||||
...tags]
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
for (const f of v.features) {
|
||||
|
||||
const src = f.properties["source"] ?? f.properties["src"] ?? v.source
|
||||
delete f.properties["source"]
|
||||
delete f.properties["src"]
|
||||
let extraNote = ""
|
||||
if (f.properties["note"]) {
|
||||
extraNote = f.properties["note"] + "\n"
|
||||
delete f.properties["note"]
|
||||
}
|
||||
|
||||
const tags: string [] = []
|
||||
for (const key in f.properties) {
|
||||
if (f.properties[key] === null || f.properties[key] === undefined) {
|
||||
console.warn("Null or undefined key for ", f.properties)
|
||||
continue
|
||||
}
|
||||
if (f.properties[key] === "") {
|
||||
continue
|
||||
}
|
||||
tags.push(key + "=" + (f.properties[key]+"").replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
|
||||
}
|
||||
|
||||
const lat = f.geometry.coordinates[1]
|
||||
const lon = f.geometry.coordinates[0]
|
||||
const text = [v.intro,
|
||||
extraNote,
|
||||
"Source: " + src,
|
||||
'More information at ' + v.wikilink,
|
||||
'',
|
||||
'Import this point easily with',
|
||||
`https://mapcomplete.osm.be/${v.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
|
||||
...tags].join("\n")
|
||||
const text = CreateNotes.createNoteContents(f, v).join("\n")
|
||||
|
||||
state.osmConnection.openNote(
|
||||
lat, lon, text)
|
||||
|
@ -62,13 +73,19 @@ export class CreateNotes extends Combine {
|
|||
}
|
||||
|
||||
super([
|
||||
new Title("Creating notes"),
|
||||
"Hang on while we are importing...",
|
||||
new Title(t.title),
|
||||
t.loading ,
|
||||
new Toggle(
|
||||
new Loading(new VariableUiElement(currentNote.map(count => new FixedUiElement("Imported <b>" + count + "</b> out of " + v.features.length + " notes")))),
|
||||
new Loading(new VariableUiElement(currentNote.map(count => t.creating.Subs({
|
||||
count, total: v.features.length
|
||||
}
|
||||
|
||||
)))),
|
||||
new Combine([
|
||||
new FixedUiElement("All done!").SetClass("thanks"),
|
||||
new SubtleButton(Svg.note_svg(), "Inspect the progress of your notes in the 'import_viewer'", {
|
||||
Svg.party_svg().SetClass("w-24"),
|
||||
t.done.Subs(v.features.length).SetClass("thanks"),
|
||||
new SubtleButton(Svg.note_svg(),
|
||||
t.openImportViewer , {
|
||||
url: "import_viewer.html"
|
||||
})
|
||||
]
|
||||
|
|
|
@ -25,15 +25,15 @@ export class FlowPanelFactory<T> {
|
|||
this._stepNames = stepNames;
|
||||
}
|
||||
|
||||
public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
|
||||
return new FlowPanelFactory(step, [], [name])
|
||||
public static start<TOut>(name:{title: BaseUIElement}, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
|
||||
return new FlowPanelFactory(step, [], [name.title])
|
||||
}
|
||||
|
||||
public then<TOut>(name: string | 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]),
|
||||
this._stepNames.concat([name])
|
||||
this._stepNames.concat([name["title"] ?? name])
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -120,11 +120,11 @@ export class FlowPanel<T> extends Toggle {
|
|||
isError.setData(true)
|
||||
}
|
||||
}),
|
||||
"Select a valid value to continue",
|
||||
new SubtleButton(Svg.invalid_svg(), t.notValid),
|
||||
initial.IsValid
|
||||
),
|
||||
new Toggle(
|
||||
new FixedUiElement("Something went wrong...").SetClass("alert"),
|
||||
t.error.SetClass("alert"),
|
||||
undefined,
|
||||
isError),
|
||||
]).SetClass("flex w-full justify-end space-x-2"),
|
||||
|
|
|
@ -7,7 +7,7 @@ import MinimapImplementation from "../Base/MinimapImplementation";
|
|||
import Translations from "../i18n/Translations";
|
||||
import {FlowPanelFactory} from "./FlowStep";
|
||||
import {RequestFile} from "./RequestFile";
|
||||
import {PreviewPanel} from "./PreviewPanel";
|
||||
import {PreviewAttributesPanel} from "./PreviewPanel";
|
||||
import ConflationChecker from "./ConflationChecker";
|
||||
import {AskMetadata} from "./AskMetadata";
|
||||
import {ConfirmProcess} from "./ConfirmProcess";
|
||||
|
@ -26,20 +26,20 @@ 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("Introduction", new Introdution())
|
||||
.then("Login", _ => new LoginToImport(state))
|
||||
.then("Select file", _ => new RequestFile())
|
||||
.then("Inspect attributes", geojson => new PreviewPanel(state, geojson))
|
||||
.then("Inspect data", geojson => new MapPreview(state, geojson))
|
||||
.then("Select theme", v => new SelectTheme(v))
|
||||
.then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v))
|
||||
.then("Compare with existing data", v => new ConflationChecker(state, v))
|
||||
.then("License and community check", v => new ConfirmProcess(v))
|
||||
.then("Metadata", (v) => new AskMetadata(v))
|
||||
.finish("Note creation", v => new CreateNotes(state, v));
|
||||
.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 => {
|
||||
|
@ -58,11 +58,11 @@ export default class ImportHelperGui extends LeftIndex {
|
|||
, true)
|
||||
|
||||
const leftContents: BaseUIElement[] = [
|
||||
new SubtleButton(undefined, "Inspect your preview imports", {
|
||||
new SubtleButton(undefined, t.gotoImportViewer, {
|
||||
url: "import_viewer.html"
|
||||
}),
|
||||
toc,
|
||||
new Toggle(new FixedUiElement("Testmode - won't actually import notes").SetClass("alert"), undefined, state.featureSwitchIsTesting),
|
||||
new Toggle(t.testMode.SetClass("block alert"), undefined, state.featureSwitchIsTesting),
|
||||
LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
|
||||
].map(el => el?.SetClass("pl-4"))
|
||||
|
||||
|
|
|
@ -3,18 +3,42 @@ import {FlowStep} from "./FlowStep";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Title from "../Base/Title";
|
||||
import {CreateNotes} from "./CreateNotes";
|
||||
|
||||
export default class Introdution extends Combine implements FlowStep<void> {
|
||||
readonly IsValid: UIEventSource<boolean> = new UIEventSource<boolean>(true);
|
||||
readonly Value: UIEventSource<void> = new UIEventSource<void>(undefined);
|
||||
readonly IsValid: UIEventSource<boolean>;
|
||||
readonly Value: UIEventSource<void>;
|
||||
|
||||
constructor() {
|
||||
const example = CreateNotes.createNoteContents({
|
||||
properties:{
|
||||
"some_key":"some_value",
|
||||
"note":"a note in the original dataset"
|
||||
},
|
||||
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"
|
||||
})
|
||||
|
||||
super([
|
||||
new Title(Translations.t.importHelper.title),
|
||||
Translations.t.importHelper.description,
|
||||
Translations.t.importHelper.importFormat,
|
||||
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")
|
||||
]);
|
||||
this.SetClass("flex flex-col")
|
||||
this. IsValid= new UIEventSource<boolean>(true);
|
||||
this. Value = new UIEventSource<void>(undefined);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ export default class LoginToImport extends Combine implements FlowStep<UserRelat
|
|||
private static readonly whitelist = [15015689];
|
||||
|
||||
constructor(state: UserRelatedState) {
|
||||
const t = Translations.t.importHelper
|
||||
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)
|
||||
|
|
|
@ -12,16 +12,16 @@ import List from "../Base/List";
|
|||
import CheckBoxes from "../Input/Checkboxes";
|
||||
|
||||
/**
|
||||
* Shows the data to import on a map, asks for the correct layer to be selected
|
||||
* Shows the attributes by value, requests to check them of
|
||||
*/
|
||||
export class PreviewPanel extends Combine implements FlowStep<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> {
|
||||
export class PreviewAttributesPanel extends Combine implements FlowStep<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> {
|
||||
public readonly IsValid: UIEventSource<boolean>;
|
||||
public readonly Value: UIEventSource<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }>
|
||||
|
||||
constructor(
|
||||
state: UserRelatedState,
|
||||
geojson: { features: { properties: any, geometry: { coordinates: [number, number] } }[] }) {
|
||||
const t = Translations.t.importHelper;
|
||||
const t = Translations.t.importHelper.previewAttributes;
|
||||
|
||||
const propertyKeys = new Set<string>()
|
||||
for (const f of geojson.features) {
|
||||
|
|
|
@ -10,12 +10,12 @@ import Title from "../Base/Title";
|
|||
import {RadioButton} from "../Input/RadioButton";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Toggleable from "../Base/Toggleable";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
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[],
|
||||
|
@ -33,7 +33,7 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
|||
public readonly IsValid: UIEventSource<boolean>;
|
||||
|
||||
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")
|
||||
|
@ -69,15 +69,15 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
|||
})
|
||||
|
||||
super([
|
||||
new Title("Select a theme"),
|
||||
"All of the following themes will show the import notes. However, the note on OpenStreetMap can link to only one single theme. Choose which theme that the created notes will link to",
|
||||
new Title(t.title),
|
||||
t.intro,
|
||||
themeRadios,
|
||||
new VariableUiElement(applicablePresets.map(applicablePresets => {
|
||||
if (themeRadios.GetValue().data === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (applicablePresets === undefined || applicablePresets.length === 0) {
|
||||
return new FixedUiElement("This theme has no presets loaded. As a result, imports won't work here").SetClass("alert")
|
||||
return t.noMatchingPresets.SetClass("alert")
|
||||
}
|
||||
}, [themeRadios.GetValue()])),
|
||||
|
||||
|
@ -115,11 +115,14 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
|||
if (unmatched === undefined || unmatched.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const applicablePresetsOverview = applicablePresets.map(preset => new Combine([
|
||||
preset.title.txt, "needs tags",
|
||||
new FixedUiElement(preset.tags.map(t => t.asHumanString()).join(" & ")).SetClass("thanks")
|
||||
]))
|
||||
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) {
|
||||
|
@ -133,20 +136,16 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
|||
const missing = []
|
||||
for (const {k, v} of tags) {
|
||||
if (preset[k] === undefined) {
|
||||
missing.push(
|
||||
`Expected ${k}=${v}, but it is completely missing`
|
||||
)
|
||||
missing.push(t.missing.Subs({k,v}))
|
||||
} else if (feat.properties[k] !== v) {
|
||||
missing.push(
|
||||
`Property with key ${k} does not have expected value ${v}; instead it is ${feat.properties}`
|
||||
)
|
||||
missing.push(t.misMatch.Subs({k, v, properties: feat.properties}))
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
parts.push(
|
||||
new Combine([
|
||||
new FixedUiElement(`Preset ${preset.title.txt} is not applicable:`),
|
||||
t.notApplicable.Subs(preset),
|
||||
new List(missing)
|
||||
]).SetClass("flex flex-col alert")
|
||||
)
|
||||
|
@ -158,9 +157,9 @@ export default class SelectTheme extends Combine implements FlowStep<{
|
|||
}
|
||||
|
||||
return new Combine([
|
||||
new FixedUiElement(unmatched.length + " objects dont match any presets").SetClass("alert"),
|
||||
t.displayNonMatchingCount.Subs(unmatched).SetClass("alert"),
|
||||
...applicablePresetsOverview,
|
||||
new Toggleable(new Title("The following elements don't match any of the presets"),
|
||||
new Toggleable(new Title(t.unmatchedTitle),
|
||||
new Combine(unmatchedPanels))
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ export default class InputElementWrapper<T> extends InputElement<T> {
|
|||
private readonly _inputElement: InputElement<T>;
|
||||
private readonly _renderElement: BaseUIElement
|
||||
|
||||
constructor(inputElement: InputElement<T>, translation: Translation, key: string, tags: UIEventSource<any>, state: FeaturePipelineState) {
|
||||
constructor(inputElement: InputElement<T>, translation: Translation, key: string,
|
||||
tags: UIEventSource<any>, state: FeaturePipelineState) {
|
||||
super()
|
||||
this._inputElement = inputElement;
|
||||
const mapping = new Map<string, BaseUIElement>()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue