Polishing and translations for the import helper

This commit is contained in:
Pieter Vander Vennet 2022-04-14 03:01:54 +02:00
parent f7844d8b2b
commit 8e2e227563
7 changed files with 161 additions and 87 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -37,9 +37,9 @@ export default class ImportHelperGui extends LeftIndex {
.then(t.selectTheme, v => new SelectTheme(v))
.then(t.compareToAlreadyExistingNotes, 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));
.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"))

View file

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