From adf4eb18c0d910dd0425e4026a8208fb6d718de1 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 26 Nov 2021 02:45:39 +0100 Subject: [PATCH] First nearly-finished draft of project page --- Professional.md | 49 ------------ UI/Base/Title.ts | 4 + UI/Base/Toggleable.ts | 24 +++++- UI/ProfessionalGui.ts | 139 ++++++++++++++++++++++++++-------- css/index-tailwind-output.css | 53 ++++++++++--- langs/en.json | 61 ++++++++++----- 6 files changed, 216 insertions(+), 114 deletions(-) diff --git a/Professional.md b/Professional.md index a82b9eafc..4732160b7 100644 --- a/Professional.md +++ b/Professional.md @@ -1,54 +1,5 @@ -What are the survey possibilities? ----- - -MapComplete is an easy to use _survey_ tool. It is ideal to collect the necessary in a few clicks, both on desktop and on mobile. This data is contributed directly into OpenStreetMap. - -We can setup a custom survey tool, asking precisely the data you need in a future-proof way. - -Do you have a dataset that has to be (re)surveyed? - -This is the ideal moment to add those to OpenStreetMap directly. -MapComplete can show your dataset and OpenStreetMap at the same time, making it easier to visit all the locations and to see what the community already contributed. - - - -Using the data in internal processes ------------------------------------- - -Your MapComplete theme can have a convenient _export_-button, offering to download the data in open formats usable in QGis, ArcGis, Excell, LibreOffice-calc, ... -Someone with basic spreadsheet-skills can thus easily create graphs and insights about the data, whereas the GIS-experts within your organisations can easily work with this data in their preferred program. - -The data can also be retrieved for free via an API-call if an automated setup is needed. - - -Some drawbacks -=========================== - -While joining this community has tremendous benefits, there are a few topics to carefully consider. - -## Data not suited for OpenStreetMap - -The basic rule for OpenStreetMap is that all data must be verifiable on the ground and are somewhat permanent. - -This implies that some data _cannot_ be sent to OpenStreetMap directly - but some workarounds exist. - -- Subjective data (such as reviews) are not suited for OpenStreetMap. However, MapComplete has an integration with Mangrove.reviews, an openly licensed review website -- Events of a few days, road works that are planned next month are thus _not_ recorded, neither are road works which only last a few days. -- Temporal data (e.g. statistics of air quality, traffic intensity, ...) can not stored on OpenStreetMap as they are hard to verify by a volunteer. Note that, if this data is available elsewhere, it can still be visualized though. - - -### Licensing nuances - -OpenStreetMap uses the Open Database License. If a contributor adds data to OpenStreetMap, this data will be republished under this license, which states that: - -1. All data can be reused for any purpose - including commercial purposes -2. Applications or products using OpenStreetMap should give a clear copyright notice -3. Any dataset or product which contains OpenStreetMap-data must be republished under ODbL too, including modifications to this dataset and in a usable format. - -This has a few implications which should be considered for some usecases: - #### Making a map from different data sources For example, one could make a map with all benches in some city, based on the benches known by OpenStreetMap. This printed map needs a clear statement that the map data is based on OpenStreetMap. diff --git a/UI/Base/Title.ts b/UI/Base/Title.ts index 31afdfc80..c9aff4c92 100644 --- a/UI/Base/Title.ts +++ b/UI/Base/Title.ts @@ -1,9 +1,11 @@ import BaseUIElement from "../BaseUIElement"; import {FixedUiElement} from "./FixedUiElement"; +import Hash from "../../Logic/Web/Hash"; export default class Title extends BaseUIElement { public readonly title: BaseUIElement; public readonly level: number; + public readonly id : string constructor(embedded: string | BaseUIElement, level: number = 3) { super() @@ -13,6 +15,7 @@ export default class Title extends BaseUIElement { this.title = embedded } this.level = level; + this.id = this.title.ConstructElement().innerText.replace(/ /g, '_') } AsMarkdown(): string { @@ -36,6 +39,7 @@ export default class Title extends BaseUIElement { } const h = document.createElement("h" + this.level) h.appendChild(el) + el.id = this.id return h; } } \ No newline at end of file diff --git a/UI/Base/Toggleable.ts b/UI/Base/Toggleable.ts index e828a5cf3..277ff41f9 100644 --- a/UI/Base/Toggleable.ts +++ b/UI/Base/Toggleable.ts @@ -1,6 +1,8 @@ import BaseUIElement from "../BaseUIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "./Combine"; +import Title from "./Title"; +import Hash from "../../Logic/Web/Hash"; export class Accordeon extends Combine { @@ -23,14 +25,32 @@ export class Accordeon extends Combine { export default class Toggleable extends Combine { public readonly isVisible = new UIEventSource(false) - constructor(title: BaseUIElement, content: BaseUIElement) { + constructor(title: Title | BaseUIElement, content: BaseUIElement) { super([title, content]) - content.SetClass("animate-height border-l-4 border-gray-300 pl-2") + content.SetClass("animate-height border-l-4 pl-2") title.SetClass("background-subtle rounded-lg") const self = this this.onClick(() => self.isVisible.setData(!self.isVisible.data)) const contentElement = content.ConstructElement() + if(title instanceof Title){ + Hash.hash.addCallbackAndRun(h => { + if(h === title.id){ + self.isVisible.setData(true) + content.RemoveClass("border-gray-300") + content.SetClass("border-red-300") + }else{ + content.SetClass("border-gray-300") + content.RemoveClass("border-red-300") + } + }) + this.isVisible.addCallbackAndRun(isVis => { + if(isVis){ + Hash.hash.setData(title.id) + } + }) + } + this.isVisible.addCallbackAndRun(isVisible => { if (isVisible) { contentElement.style.maxHeight = "100vh" diff --git a/UI/ProfessionalGui.ts b/UI/ProfessionalGui.ts index 738a656e0..41042de18 100644 --- a/UI/ProfessionalGui.ts +++ b/UI/ProfessionalGui.ts @@ -6,17 +6,19 @@ import Toggleable, {Accordeon} from "./Base/Toggleable"; import List from "./Base/List"; import BaseUIElement from "./BaseUIElement"; import Link from "./Base/Link"; +import LanguagePicker from "./LanguagePicker"; +import Hash from "../Logic/Web/Hash"; +import {Translation} from "./i18n/Translation"; +import {SubtleButton} from "./Base/SubtleButton"; +import Svg from "../Svg"; class TableOfContents extends Combine { - /** - * All the padding levels. Note that these are written out so that tailwind-generate-css can detect them - * @private - */ - private static readonly paddings: string[] = ["pl-0, pl-2", "pl-4", "pl-6", "pl-8", "pl-10", "pl-12", "pl-14"] private readonly titles: Title[] - constructor(elements: Combine | Title[]) { + constructor(elements: Combine | Title[], options: { + noTopLevel: false | boolean + }) { let titles: Title[] if (elements instanceof Combine) { titles = elements.getToC() @@ -24,21 +26,71 @@ class TableOfContents extends Combine { titles = elements } - const minLevel = Math.min(...titles.map(t => t.level)) - - const els: BaseUIElement[] = [] + let els: { level: number, content: BaseUIElement }[] = [] for (const title of titles) { - const l = title.level - minLevel - const padding = TableOfContents.paddings[l] - const text = title.title.ConstructElement().innerText - const vis = new Link(new FixedUiElement(text), "#" + text).SetClass(padding) - els.push(vis) + let content: BaseUIElement + if (title.title instanceof Translation) { + content = title.title.Clone() + } else { + content = new FixedUiElement(title.title.ConstructElement().innerText) + } + + const vis = new Link(content, "#" + title.id) + + Hash.hash.addCallbackAndRun(h => { + if (h === title.id) { + vis.SetClass("font-bold") + } else { + vis.RemoveClass("font-bold") + } + }) + els.push({level: title.level, content: vis}) + } - super(els); + if (options.noTopLevel) { + const minLevel = Math.min(...els.map(e => e.level)) + els = els.filter(e => e.level !== minLevel) + } + + + super(TableOfContents.mergeLevel(els).map(el => el.SetClass("mt-2"))); this.SetClass("flex flex-col") this.titles = titles; } + private static mergeLevel(elements: { level: number, content: BaseUIElement }[]): BaseUIElement[] { + const maxLevel = Math.max(...elements.map(e => e.level)) + const minLevel = Math.min(...elements.map(e => e.level)) + if (maxLevel === minLevel) { + return elements.map(e => e.content) + } + const result: { level: number, content: BaseUIElement } [] = [] + let running: BaseUIElement[] = [] + for (const element of elements) { + if (element.level === maxLevel) { + running.push(element.content) + continue + } + if (running.length !== undefined) { + result.push({ + content: new List(running), + level: maxLevel - 1 + }) + running = [] + } + result.push(element) + } + if (running.length !== undefined) { + result.push({ + content: new List(running), + level: maxLevel - 1 + }) + } + + return TableOfContents.mergeLevel(result) + + } + AsMarkdown(): string { return super.AsMarkdown(); } @@ -71,19 +123,18 @@ export default class ProfessionalGui { constructor() { const t = Translations.t.professional - // LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages()).SetClass("flex absolute top-2 right-3"), - const content = new Combine([ + + const header = new Combine([ + new FixedUiElement(`MapComplete Logo`) + .SetClass("flex-none m-3"), new Combine([ - new FixedUiElement(`MapComplete Logo`) - .SetClass("flex-none m-3"), - new Combine([ - - new Title(t.title, 1).SetClass("font-bold text-3xl"), - t.intro - ]).SetClass("flex flex-col") - - ]).SetClass("flex"), + new Title(t.title, 1).SetClass("font-bold text-3xl"), + t.intro + ]).SetClass("flex flex-col") + ]).SetClass("flex") + const content = new Combine([ + header, new Title(t.osmTitle, 2).SetClass("text-2xl"), t.text0, t.text1, @@ -97,13 +148,41 @@ export default class ProfessionalGui { new Title(t.aboutMc.title, 2).SetClass("text-2xl"), t.aboutMc.text0, t.aboutMc.text1, - t.aboutMc.text2 + t.aboutMc.text2, + new Accordeon([ + new Snippet(t.aboutMc.layers), + new Snippet(t.aboutMc.survey), + new Snippet(t.aboutMc.internalUse) + ]), + new Title(t.drawbacks.title, 2).SetClass("text-2xl"), + t.drawbacks.intro, + new Accordeon([ + new Snippet(t.drawbacks.unsuitedData), + new Snippet(t.drawbacks.licenseNuances) + ]), + + ]).SetClass("flex flex-col pb-12 m-3 lg:w-3/4 lg:ml-10 link-underline") - ]).SetClass("flex flex-col pb-12 m-3 lg:w-3/4 lg:ml-10") + const backToIndex = new Combine([new SubtleButton( + Svg.back_svg().SetStyle("height: 1.5rem;"), + t.backToMapcomplete, + { + url: window.location.host + "/index.html" + } + )]).SetClass("block") - const toc = new TableOfContents(content) - new Combine([toc, content]).SetClass("flex").AttachTo("main") + const leftContents: BaseUIElement[] = [ + backToIndex, + new TableOfContents(content, { + noTopLevel: true + }).SetClass("subtle"), + LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages()).SetClass("mt-4 self-end flex-col"), + ].map(el => el.SetClass("pl-4")) + const leftBar = new Combine([ + new Combine(leftContents).SetClass("sticky top-4 m-4") + ]).SetClass("block w-full md:w-2/6 lg:w-1/6") + new Combine([leftBar, content]).SetClass("block md:flex").AttachTo("main") } diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 4872ff01c..7f7961c21 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -748,6 +748,10 @@ video { right: 0.75rem; } +.top-4 { + top: 1rem; +} + .bottom-0 { bottom: 0px; } @@ -820,14 +824,14 @@ video { margin: 0.75rem; } -.m-2 { - margin: 0.5rem; -} - .m-4 { margin: 1rem; } +.m-2 { + margin: 0.5rem; +} + .m-6 { margin: 1.5rem; } @@ -864,14 +868,14 @@ video { margin-top: 0.25rem; } -.mr-4 { - margin-right: 1rem; -} - .mt-4 { margin-top: 1rem; } +.mr-4 { + margin-right: 1rem; +} + .ml-2 { margin-left: 0.5rem; } @@ -1212,6 +1216,10 @@ video { gap: 1rem; } +.self-end { + align-self: flex-end; +} + .self-center { align-self: center; } @@ -1305,6 +1313,11 @@ video { border-color: rgba(209, 213, 219, var(--tw-border-opacity)); } +.border-red-300 { + --tw-border-opacity: 1; + border-color: rgba(252, 165, 165, var(--tw-border-opacity)); +} + .border-gray-400 { --tw-border-opacity: 1; border-color: rgba(156, 163, 175, var(--tw-border-opacity)); @@ -1401,6 +1414,10 @@ video { padding-bottom: 3rem; } +.pl-4 { + padding-left: 1rem; +} + .pl-2 { padding-left: 0.5rem; } @@ -1433,10 +1450,6 @@ video { padding-right: 0.75rem; } -.pl-4 { - padding-left: 1rem; -} - .pr-4 { padding-right: 1rem; } @@ -2405,6 +2418,10 @@ li::marker { display: block; } + .md\:flex { + display: flex; + } + .md\:grid { display: grid; } @@ -2421,6 +2438,10 @@ li::marker { max-height: 65vh; } + .md\:w-2\/6 { + width: 33.333333%; + } + .md\:w-auto { width: auto; } @@ -2505,10 +2526,18 @@ li::marker { margin-left: 10rem; } + .lg\:ml-10 { + margin-left: 2.5rem; + } + .lg\:w-3\/4 { width: 75%; } + .lg\:w-1\/6 { + width: 16.666667%; + } + .lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } diff --git a/langs/en.json b/langs/en.json index 47d15ae70..f2dd465d2 100644 --- a/langs/en.json +++ b/langs/en.json @@ -319,20 +319,20 @@ "surveillance": "As you are reading the privacy policy, you probably care about privacy - so do we! We even made a theme showing surveillance cameras. Feel free to map them all!" }, "professional": { + "backToMapcomplete": "Back to the theme overview", "title": "Professional support with MapComplete", "intro": "The developer of MapComplete offers professional support. This document outlines some of the possibilities, common questions and the boundaries of MapComplete", "osmTitle": "What can OpenStreetMap and MapComplete do for your organisation?", "text0": "

Maintaining a set of up-to-date geodata is hard, error prone and expensive.
To add insult to injury, many organizations end up collecting the same data independently - resulting in duplicated efforts, non-standardized data formats and many incomplete, unmaintained datasets.

At the same time, there is a huge community which gathers a lot of geodata into one shared, global and standardized database - namely OpenStreetMap.org.

", "text1": "

MapComplete is the editor to make contributing data to OpenStreetMap easy.

", - "aboutOsm": { "aboutOsm": { - "title": "What is OpenStreetMap?", - "intro": "OpenStreetMap is a shared, global database, built by volunteers. All geodata can be contributed to OpenStreetMap, as long as it can be verified on the ground.
OpenStreetMap has grown to be a very broad and deep dataset as it contains data over thousands of categories of objects.An individual object might also have a ton of attributes, bringing a lot of nuance, e.g.:", - "li0": "Streets have geometry, but might also have information about the maxspeed, surface, wether they are lit, their name, a link to Wikipedia, a link to what they are named after, which hiking-, cycle- and busroutes run over theme", - "li1": "Shops and other amenities might have opening hours, a phone number, a link to the website, which payment methods are supported, what they sell, which services they offer, ...", - "li2": "Toilets might have information about wheelchair accessibility, a changing table, if payment is needed, ...", - "li3": "and much, much more..." + "title": "What is OpenStreetMap?", + "intro": "OpenStreetMap is a shared, global database, built by volunteers. All geodata can be contributed to OpenStreetMap, as long as it can be verified on the ground.
OpenStreetMap has grown to be a very broad and deep dataset as it contains data over thousands of categories of objects.An individual object might also have a ton of attributes, bringing a lot of nuance, e.g.:", + "li0": "Streets have geometry, but might also have information about the maxspeed, surface, wether they are lit, their name, a link to Wikipedia, a link to what they are named after, which hiking-, cycle- and busroutes run over theme", + "li1": "Shops and other amenities might have opening hours, a phone number, a link to the website, which payment methods are supported, what they sell, which services they offer, ...", + "li2": "Toilets might have information about wheelchair accessibility, a changing table, if payment is needed, ...", + "li3": "and much, much more..." }, "benefits": { "title": "Benefits of the OSM-ecosystem", @@ -360,22 +360,41 @@ }, "aboutMc": { "title": "Using MapComplete in your organization", - "text0": "If an existing MapComplete theme is what you need to survey data or to show on your website, feel free to embed it. Embedding the public themes is free and always will be.", - "text1": "Do you need some other data, but does the theme not exist yet? The MapComplete-developers can build it for you on a decent budget. Get in touch via email, github or send a message via osm.org", + "text0": "If an existing MapComplete theme is what you, feel free to use it or embed it on your website. Embedding the public themes is free and always will be.", + "text1": "Do you need some other data, but does the theme not exist yet? The MapComplete-developers can build it for you on a decent budget. Get in touch via email, github or send a message via osm.org", "text2": "If you still feel unsure, the possibilities are outlined below. Additionally, some common questions are answered", -"layers": { - "title": "What data can be shown with MapComplete?", - "intro": "

MapComplete has a powerful templating system, which allows to quickly create a map showing precisely those features that you need and showing relevant attributes in the popups.

This data can be fetched from OpenStreetMap directly, but MapComplete can also use external datasets - e.g. to compare OpenStreetMap with another dataset or to show data that is not suited for OpenStreetMap (planned activities, statistics, ...)" -} + "layers": { + "title": "What data can be shown with MapComplete?", + "intro": "

MapComplete has a powerful templating system, which allows to quickly create a map showing precisely those features that you need and showing relevant attributes in the popups.

This data can be fetched from OpenStreetMap directly, but MapComplete can also use external datasets - e.g. to compare OpenStreetMap with another dataset or to show data that is not suited for OpenStreetMap (planned activities, statistics, ...)" + }, + "survey": { + "title": "Survey possibilities", + "intro": "

MapComplete is an easy to use survey tool. It is ideal to collect the necessary in a few clicks, both on desktop and on mobile. This data is contributed directly into OpenStreetMap.

We can setup a custom survey tool, asking precisely the data you need in a future-proof way.

Do you have a dataset that has to be (re)surveyed? This is the perfect moment to make the switch to OpenStreetMap.MapComplete can show your dataset and OpenStreetMap at the same time, making it easier to visit all the locations and to see what the community already contributed.

\n" + }, + "internalUse": { + "title": "Using the data in internal processes", + "intro": "

Once the data is in OpenStreetMap, you'll probably want to use the data as well. Your MapComplete theme can have a convenient export-button, offering to download the data in many open formats usable in QGis, ArcGis, Excel, LibreOffice-calc, ...

Someone with basic spreadsheet-skills can thus easily create graphs and insights about the data, whereas the GIS-experts within your organisation can easily work with this data in their preferred application.

If an automated setup is needed, a free-to-use, community-run API is available.

" + } + }, + "drawbacks": { + "title": "A few drawbacks to keep in mind", + "intro": "While joining this community has tremendous benefits, there are a few topics to carefully consider.", + "unsuitedData": { + "title": "Data not suited for OpenStreetMap", + "intro": "The basic rule for OpenStreetMap is that all data must be verifiable on the ground and are somewhat permanent. This implies that some data cannot be sent to OpenStreetMap directly - but some workarounds exist.", + "li0": "Subjective data (such as reviews) are not suited for OpenStreetMap. However, MapComplete has an integration with Mangrove.reviews, an openly licensed review website", + "li1": "Events of a few days, road works that are planned next month are thus not recorded, neither are road works which only last a few days.", + "li2": "Temporal data (e.g. statistics of air quality, traffic intensity, ...) can not stored on OpenStreetMap as they are hard to verify by a volunteer. Note that, if this data is available elsewhere, it can still be visualized within MapComplete as extra layer." + }, + "licenseNuances": { + "title": "Implications of ODbL: some use cases", + "intro": "OpenStreetMap is licensed unter the Open Database License which states that:", + "li0": "All data can be reused for any purpose - including commercial purposes", + "li1": "Applications or products using OpenStreetMap should give a clear copyright notice", + "li2": "Any dataset or product which contains OpenStreetMap-data must be republished under ODbL too, including modifications to this dataset and in a usable format.", + "outro": "This has a few implications which should be considered for some usecases, as explained below" + } - - - - - - } - - } }