diff --git a/Customizations/TagRendering.ts b/Customizations/TagRendering.ts index bb1f2ed..53d4d60 100644 --- a/Customizations/TagRendering.ts +++ b/Customizations/TagRendering.ts @@ -15,6 +15,7 @@ import Locale from "../UI/i18n/Locale"; import {State} from "../State"; import {TagRenderingOptions} from "./TagRenderingOptions"; import Translation from "../UI/i18n/Translation"; +import {SubtleButton} from "../UI/Base/SubtleButton"; export class TagRendering extends UIElement implements TagDependantUIElement { @@ -39,6 +40,8 @@ export class TagRendering extends UIElement implements TagDependantUIElement { private readonly _questionElement: InputElement; private readonly _saveButton: UIElement; + private readonly _friendlyLogin: UIElement; + private readonly _skipButton: UIElement; private readonly _editButton: UIElement; @@ -142,14 +145,17 @@ export class TagRendering extends UIElement implements TagDependantUIElement { if (tags === undefined) { return ""; } - if ((State.state?.osmConnection?.userDetails?.data?.csCount ?? 0) < 200) { + const csCount = State.state.osmConnection.userDetails.data.csCount; + if (csCount < State.userJourney.tagsVisibleAt) { return ""; } - return tags.asHumanString() + if (csCount < State.userJourney.tagsVisibleAndWikiLinked) { + return new FixedUiElement(tags.asHumanString(false)).SetClass("subtle").Render(); + } + return tags.asHumanString(true); } ) ); - this._appliedTags.clss = "subtle"; const cancel = () => { self._questionSkipped.setData(true); @@ -161,6 +167,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement { this._saveButton = new SaveButton(this._questionElement.GetValue()) .onClick(save); + this._friendlyLogin = Translations.t.general.loginToStart + .onClick(() => State.state.osmConnection.AttemptLogin()) + this._editButton = new FixedUiElement(""); if (this._question !== undefined) { this._editButton = new FixedUiElement("edit") @@ -381,6 +390,17 @@ export class TagRendering extends UIElement implements TagDependantUIElement { InnerRender(): string { + + if (this.IsQuestioning() && !State.state.osmConnection.userDetails.data.loggedIn) { + const question = + this.ApplyTemplate(this._question).Render(); + return "
" + + "" + question + "" + + "
" + + "" + + "
" + } + if (this.IsQuestioning() || this._editMode.data) { // Not yet known or questioning, we have to ask a question @@ -430,14 +450,14 @@ export class TagRendering extends UIElement implements TagDependantUIElement { } const self = this; const tags = this._source.map(tags => self._tagsPreprocessor(self._source.data)); - let transl : Translation; - if (typeof (template) === "string") { - transl = new Translation({en: TagUtils.ApplyTemplate(template, tags)}); - }else{ - transl = template; - } - - return new VariableUiElement(tags.map(tags => transl.Subs(tags).InnerRender())); + return new VariableUiElement(tags.map(tags => { + const tr = Translations.WT(template); + if (tr.Subs === undefined) { + // This is a weird edge case + return tr.InnerRender(); + } + return tr.Subs(tags).InnerRender() + })); } diff --git a/InitUiElements.ts b/InitUiElements.ts index 4bfcd5b..1a70e0d 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -64,8 +64,10 @@ export class InitUiElements { tabs.push({header: ``, content: new ShareScreen()}); } - if (State.state.featureSwitchMoreQuests.data) { - tabs.push({header: ``, content: new MoreScreen()}); + if (State.state.featureSwitchMoreQuests.data){ + + tabs.push({header: `` + , content: new MoreScreen()}); } diff --git a/Logic/TagsFilter.ts b/Logic/TagsFilter.ts index fdff5e0..ad62049 100644 --- a/Logic/TagsFilter.ts +++ b/Logic/TagsFilter.ts @@ -8,7 +8,7 @@ export abstract class TagsFilter { return this.matches(TagUtils.proprtiesToKV(properties)); } - abstract asHumanString(); + abstract asHumanString(linkToWiki: boolean); } @@ -150,8 +150,13 @@ export class Tag extends TagsFilter { return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags)); } - asHumanString() { - return this.key+"="+this.value; + asHumanString(linkToWiki: boolean) { + if (linkToWiki) { + return `${this.key}` + + `=` + + `${this.value}` + } + return this.key + "=" + this.value; } } @@ -200,9 +205,9 @@ export class Or extends TagsFilter { } return new Or(newChoices); } - - asHumanString() { - return this.or.map(t => t.asHumanString()).join("|"); + + asHumanString(linkToWiki: boolean) { + return this.or.map(t => t.asHumanString(linkToWiki)).join("|"); } } @@ -262,8 +267,8 @@ export class And extends TagsFilter { return new And(newChoices); } - asHumanString() { - return this.and.map(t => t.asHumanString()).join("&"); + asHumanString(linkToWiki: boolean) { + return this.and.map(t => t.asHumanString(linkToWiki)).join("&"); } } @@ -288,8 +293,8 @@ export class Not extends TagsFilter{ return new Not(this.not.substituteValues(tags)); } - asHumanString() { - return "!"+this.not.asHumanString(); + asHumanString(linkToWiki: boolean) { + return "!" + this.not.asHumanString(linkToWiki); } } diff --git a/README.md b/README.md index 1eff143..a0d5228 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ The design goals of MapComplete are to be: - Easy to use, both on web and on mobile - Easy to deploy (by not having a backand) -- Easy to modify +- Easy to set up a custom theme +- Easy to fall down the rabbit hole of OSM The basic functionality is to download some map features from Overpass and then ask certain questions. An answer is sent back to directly to OpenStreetMap. @@ -26,6 +27,31 @@ An explicit non-goal of MapComplete is to modify geometries of ways. Although ad Have a theme idea? Drop it in the [issues](https://github.com/pietervdvn/MapComplete/issues) +## User journey + +MapComplete is set up to lure people into OpenStreetMap and to teach them while they are on the go, step by step. + +A typical user journey would be: + +0) Oh, this is a cool map of _my specific interest_! There is a lot of data already... +0a) The user might discover the explanation about OSM in the dedicated tab page +0b) The user might discover the other themes in the other tab +0c) The user might share the map and/or embed it + +1) The user clicks that big tempting button 'login' in order to answer questions. The user makes an account - a big step. + +2) The user answers a question! Hooray! + When at least one question is answered (aka: having one changeset on OSM), adding a new point is unlocked + +3) The user adds a new POI somewhere +3a) Note that _all messages_ must be read before being able to add a point. In other words, sending a message to a misbehaving MapComplete user acts as having a zero-minutes-block. This is added deliberately to avoid new users fucking up too much + +4) At 50 changesets, the custom layout becomes available +5) At 200 changesets, the tags become visible when answering questions and when adding a new point from a preset. This is to give more control to power users and to teach new users the tagging scheme +5) At 250 changesets, the tags get linked to the wiki +6) At 500 changesets, I expect users to be power users and to be comfortable with tagging scheme and such. The custom theme generator is unlocked. + + ## License GPL + pingback. @@ -88,6 +114,11 @@ TODO: erase cookies of third party websites and API's Help to translate mapcomplete. Fork this project, open [the file containing all translations](https://github.com/pietervdvn/MapComplete/blob/master/UI/i18n/Translations.ts), add your language and send a pull request. +# Creating your own theme + +You can create [your own theme too](https://pietervdvn.github.io/MapComplete/customGenerator.html) + + # Attributions: Data from OpenStreetMap diff --git a/State.ts b/State.ts index ea60c12..cecd332 100644 --- a/State.ts +++ b/State.ts @@ -24,7 +24,17 @@ export class State { // The singleton of the global state public static state: State; - public static vNumber = "0.0.5d"; + public static vNumber = "0.0.5e"; + + // The user journey states thresholds when a new feature gets unlocked + public static userJourney = { + customLayoutUnlock: 50, + themeGeneratorUnlock: 500, + tagsVisibleAt: 200, + tagsVisibleAndWikiLinked: 250 + + + }; public static runningFromConsole: boolean = false; diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 3b24749..40c18c7 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -15,5 +15,5 @@ export class VariableUiElement extends UIElement { InnerRender(): string { return this._html.data; } - + } \ No newline at end of file diff --git a/UI/CustomThemeGenerator/ThemeGenerator.ts b/UI/CustomThemeGenerator/ThemeGenerator.ts index 5277f07..cb8f41b 100644 --- a/UI/CustomThemeGenerator/ThemeGenerator.ts +++ b/UI/CustomThemeGenerator/ThemeGenerator.ts @@ -18,6 +18,7 @@ import {Tag} from "../../Logic/TagsFilter"; import {DropDown} from "../Input/DropDown"; import {TagRendering} from "../../Customizations/TagRendering"; import {LayerDefinition} from "../../Customizations/LayerDefinition"; +import {State} from "../../State"; TagRendering.injectFunction(); @@ -620,8 +621,8 @@ export class ThemeGenerator extends UIElement { if (!this.userDetails.data.loggedIn) { return new Combine(["Not logged in. You need to be logged in to create a theme.", this.loginButton]).Render(); } - if (this.userDetails.data.csCount < 500) { - return "You need at least 500 changesets to create your own theme."; + if (this.userDetails.data.csCount < State.userJourney.themeGeneratorUnlock ) { + return `You need at least ${State.userJourney.themeGeneratorUnlock} changesets to create your own theme.`; } diff --git a/UI/FeatureInfoBox.ts b/UI/FeatureInfoBox.ts index 361b9a2..f841901 100644 --- a/UI/FeatureInfoBox.ts +++ b/UI/FeatureInfoBox.ts @@ -98,7 +98,7 @@ export class FeatureInfoBox extends UIElement { info.push(infobox); } else if (infobox.IsQuestioning()) { questions.push(infobox); - } else if(infobox.IsSkipped()){ + } else if (infobox.IsSkipped()) { // This question is neither known nor questioning -> it was skipped skippedQuestions++; } @@ -107,7 +107,19 @@ export class FeatureInfoBox extends UIElement { let questionsHtml = ""; - if (State.state.osmConnection.userDetails.data.loggedIn && questions.length > 0) { + if (!State.state.osmConnection.userDetails.data.loggedIn) { + let mostImportantQuestion; + let score = -1000; + for (const question of questions) { + + if (mostImportantQuestion === undefined || question.Priority() > score) { + mostImportantQuestion = question; + score = question.Priority(); + } + } + + questionsHtml = mostImportantQuestion.Render(); + } else if (questions.length > 0) { // We select the most important question and render that one let mostImportantQuestion; let score = -1000; diff --git a/UI/MoreScreen.ts b/UI/MoreScreen.ts index e1dc2c9..a9cc9d0 100644 --- a/UI/MoreScreen.ts +++ b/UI/MoreScreen.ts @@ -2,11 +2,6 @@ import {UIElement} from "./UIElement"; import {VerticalCombine} from "./Base/VerticalCombine"; import Translations from "./i18n/Translations"; import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import {Utils} from "../Utils"; -import {link} from "fs"; -import {UIEventSource} from "./UIEventSource"; -import {VariableUiElement} from "./Base/VariableUIElement"; import Combine from "./Base/Combine"; import {SubtleButton} from "./Base/SubtleButton"; import {State} from "../State"; @@ -21,6 +16,7 @@ export class MoreScreen extends UIElement { } InnerRender(): string { + const tr = Translations.t.general.morescreen; const els: UIElement[] = [] @@ -36,7 +32,8 @@ export class MoreScreen extends UIElement { if (!State.state.osmConnection.userDetails.data.loggedIn) { continue; } - if (State.state.osmConnection.userDetails.data.csCount < 50) { + if (State.state.osmConnection.userDetails.data.csCount < + State.userJourney.customLayoutUnlock) { continue; } } diff --git a/UI/SimpleAddUI.ts b/UI/SimpleAddUI.ts index 1d96335..b15ca9d 100644 --- a/UI/SimpleAddUI.ts +++ b/UI/SimpleAddUI.ts @@ -9,6 +9,7 @@ import {State} from "../State"; import {UIEventSource} from "../Logic/UIEventSource"; import {UserDetails} from "../Logic/Osm/OsmConnection"; +import {FixedUiElement} from "./Base/FixedUiElement"; /** * Asks to add a feature at the last clicked location, at least if zoom is sufficient @@ -58,18 +59,26 @@ export class SimpleAddUI extends UIElement { } else { icon = preset.icon; } - }else{ - console.warn("No icon defined for preset ", preset, "in layer ",layer.layerDef.id) + } else { + console.warn("No icon defined for preset ", preset, "in layer ", layer.layerDef.id) } - const button = + const csCount = State.state.osmConnection.userDetails.data.csCount; + let tagInfo = ""; + if (csCount > State.userJourney.tagsVisibleAt) { + tagInfo = preset.tags.map(t => t.asHumanString(false)).join("&"); + tagInfo = `
${tagInfo}` + } + const button: UIElement = new SubtleButton( icon, new Combine([ "", preset.title, - "
", - preset.description !== undefined ? preset.description : ""]) + "", + preset.description !== undefined ? new Combine(["
", preset.description]) : "", + tagInfo + ]) ).onClick( () => { self.confirmButton = new SubtleButton(icon, @@ -87,10 +96,12 @@ export class SimpleAddUI extends UIElement { icon: icon }); } - ) + ) + + - this._addButtons.push(button); + this._addButtons.push(button); } } @@ -120,15 +131,23 @@ export class SimpleAddUI extends UIElement { if (this._confirmPreset.data !== undefined) { if(userDetails.data.dryRun){ - this.CreatePoint(this._confirmPreset.data.tags, this._confirmPreset.data.layerToAddTo)(); - return ""; + // this.CreatePoint(this._confirmPreset.data.tags, this._confirmPreset.data.layerToAddTo)(); + // return ""; } + let tagInfo = ""; + const csCount = State.state.osmConnection.userDetails.data.csCount; + if (csCount > State.userJourney.tagsVisibleAt) { + tagInfo = this._confirmPreset.data .tags.map(t => t.asHumanString(csCount > State.userJourney.tagsVisibleAndWikiLinked)).join("&"); + tagInfo = `
More information about the preset: ${tagInfo}` + } + return new Combine([ Translations.t.general.add.confirmIntro.Subs({title: this._confirmPreset.data.name}), userDetails.data.dryRun ? "TESTING - changes won't be saved":"", this.confirmButton, - this.cancelButton + this.cancelButton, + tagInfo ]).Render(); diff --git a/UI/UIElement.ts b/UI/UIElement.ts index 6df394a..f096a0f 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -140,7 +140,12 @@ export abstract class UIElement extends UIEventSource{ public IsEmpty(): boolean { return this.InnerRender() === ""; } - + + public SetClass(clss: string): UIElement { + this.clss = clss; + return this; + } + } diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index 10569d1..f9f1a60 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -673,6 +673,10 @@ export default class Translations { nl: "Je bent aangemeld. Welkom terug!", fr: "Vous ĂȘtes connectĂ©, bienvenue" }), + loginToStart: new T({ + en: "Login to answer this question", + nl: "Meld je aan om deze vraag te beantwoorden" + }), search: { search: new Translation({ en: "Search a location", diff --git a/assets/themes/toilets/toilets.json b/assets/themes/toilets/toilets.json index b8d5220..43c885f 100644 --- a/assets/themes/toilets/toilets.json +++ b/assets/themes/toilets/toilets.json @@ -28,6 +28,11 @@ "title": "Toilet", "tags": "amenity=toilets", "description": "Only add public toilets" + }, + { + "title": "Toilets with wheelchair accessible toilet", + "tags": "amenity=toilets&wheelchair=yes", + "description": "A restroom which has at least one wheelchair-accessible toilet" } ], "tagRenderings": [ diff --git a/index.css b/index.css index 6a7e6cd..9ad07b0 100644 --- a/index.css +++ b/index.css @@ -1111,13 +1111,28 @@ form { background-color: #3a3aeb; color: white; padding: 0.2em; - padding-left: 0.3em; - padding-right: 0.3em; + padding-left: 0.6em; + padding-right: 0.6em; font-size: x-large; font-weight: bold; border-radius: 1.5em; } +.login-button-friendly { + display: inline-block; + border: solid white 2px; + background-color: #3a3aeb; + color: white; + padding: 0.2em; + padding-left: 0.6em; + padding-right: 0.6em; + font-size: large; + font-weight: bold; + border-radius: 1.5em; + box-sizing: border-box; + width: 100%; +} + .save-non-active { display: inline-block; border: solid lightgrey 2px;