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

View file

@ -119,6 +119,7 @@
"title": "Download visible data",
"uploadGpx": "Upload your track to OpenStreetMap"
},
"error": "Something went wrong...",
"example": "Example",
"examples": "Examples",
"fewChangesBefore": "Please, answer a few questions of existing points before adding a new point.",
@ -151,6 +152,7 @@
"next": "Next",
"noNameCategory": "{category} without a name",
"noTagsSelected": "No tags selected",
"notValid": "Select a valid value to continue",
"number": "number",
"oneSkippedQuestion": "One question is skipped",
"openStreetMapIntro": "<h3>An Open Map</h3><p>One that everyone can use and edit freely. A single place to store all geo-info. Different, small, incompatible and outdated maps are not needed anywhere.</p><p><b><a href='https://OpenStreetMap.org' target='_blank'>OpenStreetMap</a></b> is not the enemy map. The map data can be used freely (with <a href='https://osm.org/copyright' target='_blank'>attribution and publication of changes to that data</a>). Everyone can add new data and fix errors. This website uses OpenStreetMap. All the data is from there, and your answers and corrections are used all over.</p><p>Many people and apps already use OpenStreetMap: <a href='https://organicmaps.app/' target='_blank'>Organic Maps</a>, <a href='https://osmAnd.net' target='_blank'>OsmAnd</a>, but also the maps at Facebook, Instagram, Apple-maps and Bing-maps are (partly) powered by OpenStreetMap.</p>",
@ -276,6 +278,18 @@
"willBePublished": "Your picture will be published "
},
"importHelper": {
"askMetadata": {
"downloadGeojson": "Download geojson",
"giveDescription": "Please, write a small description for someone who sees the note. A good note describes what the contributor has to do, e.g; <i>There might be a bench here. If you are around, could you please check and indicate if the bench exists or not?</i> (A link to MapComplete will be added automatically)",
"giveSource": "What is the source of this data? If 'source' is set in the feature, this value will be ignored",
"giveWikilink": "On what wikipage can one find more information about this import?",
"intro": "Before adding {count} notes, please provide some extra information.",
"orDownload": "Alternatively, you can download the dataset to import directly",
"shouldBeOsmWikilink": "Expected a link to a page on wiki.openstreetmap.org",
"shouldBeUrl": "Not a valid URL",
"shouldNotBeHomepage": "Nope, the home page isn't allowed either. Enter the URL of a proper wikipage documenting your import",
"title": "Set metadata"
},
"compareToAlreadyExistingNotes": {
"completelyImported": "All of the proposed points have (or had) an import note already",
"loading": "Fetching notes from OSM",
@ -288,13 +302,27 @@
"titleLong": "Compare with already existing 'to-import'-notes",
"wontBeImported": "These data points will <i>not</i> be imported and are shown as red dots on the map below"
},
"inspectDidAutoDected": "Layer was chosen automatically",
"confirmProcess": {
"contactedCommunity": "I did contact the (local) community about this import",
"licenseIsCompatible": "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",
"readImportGuidelines": "I have read the import guidelines on the OSM wiki",
"title": "License and community",
"titleLong": "Did you go through the import process?",
"wikipageIsMade": "The process is documented on the OSM-wiki (you'll need this link later)"
},
"createNotes": {
"creating": "Created <b>{count}</b> notes out of {total}",
"done": "All {count} notes have been created!",
"loading": "Hang on while we are loading...",
"openImportViewer": "Inspect the progress of your notes in the 'import_viewer'",
"title": "Note creation"
},
"gotoImportViewer": "Inspect your previous imports",
"introduction": {
"description": "The import helper converts an external dataset to notes. The external dataset must match one of the existing MapComplete layers. For every item you put in the importer, a single note will be created. These notes will be shown together with the relevant features in these maps to easily add them.",
"importFormat": "A text in a note should have the following format in order to be picked up: <br/><div class='literal-code'>[A bit of introduction]<br/>https://mapcomplete.osm.be/[themename].html?[parameters such as lat and lon]#import<br/>[all tags of the feature] </div>",
"importFormat": "A text in a note should have the following format in order to be picked up",
"title": "Introduction"
},
"locked": "You need at least {importHelperUnlock} to use the import helper",
"login": {
"lockNotice": "This page is locked. You need {importHelperUnlock} changesets before you can access here.",
"loggedInWith": "You are currently logged in as <b>{name}</b> and have made {csCount} changesets",
@ -310,6 +338,11 @@
"selectLayer": "Which layer does this import match with?",
"title": "Map preview"
},
"noteParts": {
"datasource": "Original data from {source}",
"importEasily": "Add this point easily with MapComplete:",
"wikilink": "More information about this import can be found at {wikilink}"
},
"previewAttributes": {
"allAttributesSame": "All features to import have this tag",
"inspectDataTitle": "Inspect data of {count} features to import",
@ -342,8 +375,8 @@
"title": "Select a theme",
"unmatchedTitle": "The following elements don't match any of the presets"
},
"title": "Import helper",
"validateDataTitle": "Validate data"
"testMode": "Testmode - won't actually import notes",
"title": "Import helper"
},
"importInspector": {
"title": "Inspect and manage import notes"