Studio: theme editing

This commit is contained in:
Pieter Vander Vennet 2023-10-30 13:45:44 +01:00
parent 6e7eccf9de
commit 3aa9a21dea
34 changed files with 975 additions and 350 deletions

View file

@ -246,6 +246,10 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
console.log("Removing old background in", json.id)
}
if (typeof oldThemeConfig.credits === "string") {
oldThemeConfig.credits = [oldThemeConfig.credits]
}
if (oldThemeConfig["roamingRenderings"] !== undefined) {
if (oldThemeConfig["roamingRenderings"].length == 0) {
delete oldThemeConfig["roamingRenderings"]

View file

@ -166,7 +166,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const state = this._state
json.layers = [...json.layers]
json.layers = [...(json.layers ?? [])]
const alreadyLoaded = new Set(json.layers.map((l) => l["id"]))
for (const layerName of Constants.added_by_default) {
@ -480,6 +480,20 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
if (json.hideFromOverview === true) {
return json
}
if ((json.layers ?? []).length === 0) {
context
.enter("layers")
.err(
"No layers are defined. You must define at least one layer to have a valid theme"
)
return json
}
if (!Array.isArray(json.layers)) {
context
.enter("layers")
.err("Can not iterate over layers in theme, it is a " + JSON.stringify(json.layers))
return json
}
for (const layer of json.layers) {
if (typeof layer === "string") {
continue
@ -537,7 +551,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const result = super.convert(json, context)
if (this.state.publicLayers.size === 0) {
if ((this.state.publicLayers?.size ?? 0) === 0) {
// THis is a bootstrapping run, no need to already set this flag
return result
}

View file

@ -129,7 +129,7 @@ export class DoesImageExist extends DesugaringStep<string> {
}
}
class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
/**
* The paths where this layer is originally saved. Triggers some extra checks
* @private
@ -176,6 +176,9 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
}
}
}
if (!json.title) {
context.enter("title").err(`The theme ${json.id} does not have a title defined.`)
}
if (this._isBuiltin && this._extractImages !== undefined) {
// Check images: are they local, are the licenses there, is the theme icon square, ...
const images = this._extractImages.convert(json, context.inOperation("ValidateTheme"))
@ -249,6 +252,20 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
new DetectDuplicatePresets().convert(theme, context)
}
if (!theme.title) {
context.enter("title").err("A theme must have a title")
}
if (!theme.description) {
context.enter("description").err("A theme must have a description")
}
if (theme.overpassUrl && typeof theme.overpassUrl === "string") {
context
.enter("overpassUrl")
.err("The overpassURL is a string, use a list of strings instead. Wrap it with [ ]")
}
return json
}
}

View file

@ -1,7 +1,37 @@
import { Translatable } from "./Translatable"
export default interface ExtraLinkConfigJson {
/**
* question: What icon should be shown in the link button?
* ifunset: do not show an icon
* type: icon
*/
icon?: string
text?: string | any
/**
* question: What text should be shown in the link icon?
*
* Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced
*
* ifunset: do not show a text
*/
text?: Translatable
/**
* question: if clicked, what webpage should open?
* Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced
*
* type: url
*/
href: string
/**
* question: Should the link open in a new tab?
* iftrue: Open in a new tab
* iffalse: do not open in a new tab
* ifunset: do not open in a new tab
*/
newTab?: false | boolean
/**
* question: When should the extra button be shown?
* suggestions: return [{if: "value=iframe", then: "When shown in an iframe"}, {if: "value=no-iframe", then: "When shown as stand-alone webpage"}, {if: "value=welcome-message", then: "When the welcome messages are enabled"}, {if: "value=iframe", then: "When the welcome messages are disabled"}]
*/
requirements?: ("iframe" | "no-iframe" | "welcome-message" | "no-welcome-message")[]
}

View file

@ -2,6 +2,7 @@ import { LayerConfigJson } from "./LayerConfigJson"
import ExtraLinkConfigJson from "./ExtraLinkConfigJson"
import { RasterLayerProperties } from "../../RasterLayerProperties"
import { Translatable } from "./Translatable"
/**
* Defines the entire theme.
@ -17,20 +18,30 @@ import { RasterLayerProperties } from "../../RasterLayerProperties"
*/
export interface LayoutConfigJson {
/**
* The id of this layout.
* question: What is the id of this layout?
*
* The id is a unique string to identify the theme
*
* It should be
* - in english
* - describe the theme in a single word (or a few words)
* - all lowercase and with only [a-z] or underscores (_)
*
* This is used as hashtag in the changeset message, which will read something like "Adding data with #mapcomplete for theme #<the theme id>"
* Make sure it is something decent and descriptive, it should be a simple, lowercase string.
*
* On official themes, it'll become the name of the page, e.g.
* 'cyclestreets' which become 'cyclestreets.html'
*
* type: id
* group: basic
*/
id: string
/**
*
* Who helped to create this theme and should be attributed?
*/
credits?: string
credits?: string | string[]
/**
* Only used in 'generateLayerOverview': if present, every translation will be checked to make sure it is fully translated.
@ -40,50 +51,84 @@ export interface LayoutConfigJson {
mustHaveLanguage?: string[]
/**
* The title, as shown in the welcome message and the more-screen.
* question: What is the title of this theme?
*
* The human-readable title, as shown in the welcome message and the index page
* group: basic
*/
title: string | Record<string, string>
title: Translatable
/**
* A short description, showed as social description and in the 'more theme'-buttons.
* Note that if this one is not defined, the first sentence of 'description' is used
* group: hidden
*/
shortDescription?: string | Record<string, string>
shortDescription?: Translatable
/**
* question: How would you describe this theme?
* The description, as shown in the welcome message and the more-screen
* group: basic
*
*/
description: string | Record<string, string>
description: Translatable
/**
* A part of the description, shown under the login-button.
* group: hidden
*/
descriptionTail?: string | Record<string, string>
descriptionTail?: Translatable
/**
* The icon representing this theme.
* question: What icon should be used to represent this theme?
*
* Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...
*
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)
*
* Type: icon
* group: basic
*
*/
icon: string
/**
* Link to a 'social image' which is included as og:image-tag on official themes.
* Useful to share the theme on social media.
* See https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information$
* question: What image should be used as social image preview?
* This is included as og:image-tag on official themes.
*
* See https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information
* ifunset: use the default social image of mapcomplete (or generate one based on the icon)
* Type: image
* group: basic
*/
socialImage?: string
/**
* question: At what zoomlevel should this theme open?
* Default location and zoom to start.
* Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used
* ifunset: Use the default startzoom (0)
* type: float
* group: start_location
*/
startZoom: number
/**
* question: At what start latitude should this theme open?
* Default location and zoom to start.
* Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used
* ifunset: Use 0 as start latitude
* type: float
* group: start_location
*/
startLat: number
/**
* question: At what start longitude should this theme open?
* Default location and zoom to start.
* Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used
* ifunset: Use 0 as start longitude
* type: float
* group: start_location
*/
startLon: number
/**
@ -152,7 +197,10 @@ export interface LayoutConfigJson {
tileLayerSources?: (RasterLayerProperties & { defaultState?: true | boolean })[]
/**
* The layers to display.
* question: What layers should this map show?
* type: layer[]
* types: hidden | layer | hidden
* group: layers
*
* Every layer contains a description of which feature to display - the overpassTags which are queried.
* Instead of running one query for every layer, the query is fused.
@ -208,6 +256,7 @@ export interface LayoutConfigJson {
/**
* The URL of a custom CSS stylesheet to modify the layout
* group: advanced
*/
customCss?: string
/**
@ -223,79 +272,161 @@ export interface LayoutConfigJson {
lockLocation?: [[number, number], [number, number]] | number[][]
/**
* question: should an extra help button be shown in certain circumstances?
* Adds an additional button on the top-left of the application.
* This can link to an arbitrary location.
*
* Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced
*
* Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
* For example {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
*
* group: advanced
* ifunset: show a link to open MapComplete full screen if used in an iframe
*/
extraLink?: ExtraLinkConfigJson
/**
* If set to false, disables logging in.
* The userbadge will be hidden, all login-buttons will be hidden and editing will be disabled
* question: Should a user be able to login with OpenStreetMap?
*
* If not logged in, will not show the login buttons and hide all the editable elements.
* As such, MapComplete will become read-only and a purely visualisation tool.
*
* ifunset: Enable the possiblity to login with OpenStreetMap (default)
* iffalse: Do not enable to login with OpenStreetMap, have a read-only view of MapComplete.
* iftrue: Enable the possiblity to login with OpenStreetMap
* group: feature_switches
*/
enableUserBadge?: true | boolean
/**
* If false, hides the tab 'share'-tab in the welcomeMessage
* question: Should the tab with options to share the current screen be enabled?
*
* On can get the iFrame embed code here
*
* ifunset: Enable the sharescreen (default)
* iffalse: Do not enable the share screen
* iftrue: Enable the share screen
* group: feature_switches
*/
enableShareScreen?: true | boolean
/**
* Hides the tab with more themes in the welcomeMessage
* question: Should the user be able to switch to different themes?
*
* Typically enabled in iframes and/or on commisioned themes
*
* iftrue: enable to go back to the index page showing all themes
* iffalse: do not enable to go back to the index page showing all themes; hide the 'more themes' buttons
* ifunset: mapcomplete default: enable to go back to the index page showing all themes
* group: feature_switches
*/
enableMoreQuests?: true | boolean
/**
* If false, the layer selection/filter view will be hidden
* question: Should the user be able to enable/disable layers and to filter the layers?
*
* The corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'
* iftrue: enable the filters/layers pane
* iffalse: do not enable to filter or to disable layers; hide the 'filter' tab from the overview and the button at the bottom-left
* ifunset: mapcomplete default: enable to filter or to enable/disable layers
* group: feature_switches
*/
enableLayers?: true | boolean
/**
* If set to false, hides the search bar
* question: Should the user be able to search for locations?
*
* ifunset: MapComplete default: allow to search
* iftrue: Allow to search
* iffalse: Do not allow to search; hide the search-bar
* group: feature_switches
*/
enableSearch?: true | boolean
/**
* If set to false, the ability to add new points or nodes will be disabled.
* Editing already existing features will still be possible
* question: Should the user be able to add new points?
*
* Adding new points is only possible if the loaded layers have presets set.
* Some layers do not have presets. If the theme only has layers without presets, then adding new points will not be possible.
*
* ifunset: MapComplete default: allow to create new points
* iftrue: Allow to create new points
* iffalse: Do not allow to create new points, even if the layers in this theme support creating new points
* group: feature_switches
*/
enableAddNewPoints?: true | boolean
/**
* If set to false, the 'geolocation'-button will be hidden.
* question: Should the user be able to use their GPS to geolocate themselfes on the map?
* ifunset: MapComplete default: allow to use the GPS
* iftrue: Allow to use the GPS
* iffalse: Do not allow to use the GPS, hide the geolocation-buttons
* group: feature_switches
*/
enableGeolocation?: true | boolean
/**
* Enable switching the backgroundlayer.
* If false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well
*
* question: Should the user be able to switch the background layer?
*
* iftrue: Allow to switch the background layer
* iffalse: Do not allow to switch the background layer
* ifunset: MapComplete default: Allow to switch the background layer
* group: feature_switches
*/
enableBackgroundLayerSelection?: true | boolean
/**
* If set to true, will show _all_ unanswered questions in a popup instead of just the next one
* question: Should the questions about a feature be presented one by one or all at once?
* iftrue: Show all unanswered questions at the same time
* iffalse: Show unanswered questions one by one
* ifunset: MapComplete default: Use the preference of the user to show questions at the same time or one by one
* group: feature_switches
*/
enableShowAllQuestions?: false | boolean
/**
* If set to true, download button for the data will be shown (offers downloading as geojson and csv)
* question: Should the 'download as CSV'- and 'download as Geojson'-buttons be enabled?
* iftrue: Enable the option to download the map as CSV and GeoJson
* iffalse: Enable the option to download the map as CSV and GeoJson
* ifunset: MapComplete default: Enable the option to download the map as CSV and GeoJson
* group: feature_switches
*/
enableDownload?: true | boolean
/**
* If set to true, exporting a pdf is enabled
* question: Should the 'download as PDF'-button be enabled?
* iftrue: Enable the option to download the map as PDF
* iffalse: Enable the option to download the map as PDF
* ifunset: MapComplete default: Enable the option to download the map as PDF
* group: feature_switches
*/
enablePdfDownload?: true | boolean
/**
* question: Should the 'notes' from OpenStreetMap be loaded and parsed for import helper notes?
* If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),
* these notes will be shown if a relevant layer is present.
*
* Default is true for official layers and false for unofficial (sideloaded) layers
* ifunset: MapComplete default: do not load import notes for sideloaded themes but do load them for official themes
* iftrue: Load notes and show import notes
* iffalse: Do not load import notes
* group: advanced
*/
enableNoteImports?: true | boolean
/**
* Set one or more overpass URLs to use for this theme..
* question: What overpass-api instance should be used for this layout?
*
* ifunset: Use the default, builtin collection of overpass instances
* group: advanced
*/
overpassUrl?: string | string[]
overpassUrl?: string[]
/**
* Set a different timeout for overpass queries - in seconds. Default: 30s
* question: After how much seconds should the overpass-query stop?
* If a query takes too long, the overpass-server will abort.
* Once can set the amount of time before overpass gives up here.
* ifunset: use the default amount of 30 seconds as timeout
* type: pnat
* group: advanced
*/
overpassTimeout?: number
@ -303,7 +434,8 @@ export interface LayoutConfigJson {
* Enables tracking of all nodes when data is loaded.
* This is useful for the 'ImportWay' and 'ConflateWay'-buttons who need this database.
*
* Note: this flag will be automatically set.
* Note: this flag will be automatically set and can thus be ignored.
* group: hidden
*/
enableNodeDatabase?: boolean
}

View file

@ -29,6 +29,10 @@ export default class LayoutConfig implements LayoutInformation {
public static readonly defaultSocialImage = "assets/SocialImage.png"
public readonly id: string
public readonly credits?: string
/**
* The languages this theme supports.
* Defaults to all languages the title has
*/
public readonly language: string[]
public readonly title: Translation
public readonly shortDescription: Translation
@ -81,6 +85,10 @@ export default class LayoutConfig implements LayoutInformation {
definitionRaw?: string
}
) {
console.log("Initing theme", { json, official, options })
if (json === undefined) {
throw "Cannot construct a layout config, the parameter 'json' is undefined"
}
this.official = official
this.id = json.id
this.definedAtUrl = options?.definedAtUrl
@ -94,11 +102,11 @@ export default class LayoutConfig implements LayoutInformation {
}
}
const context = this.id
this.credits = json.credits
if(!json.title){
throw `The theme ${json.id} does not have a title defined.`
}
this.language = json.mustHaveLanguage ?? Object.keys(json.title)
this.credits = typeof json.credits === "string" ? json.credits : json.credits?.join(", ")
this.language = Array.from(
new Set((json.mustHaveLanguage ?? []).concat(Object.keys(json.title ?? {})))
)
this.usedImages = Array.from(
new ExtractImages(official, undefined)
.convertStrict(json, ConversionContext.construct([json.id], ["ExtractImages"]))
@ -113,7 +121,7 @@ export default class LayoutConfig implements LayoutInformation {
)} which is a ${typeof json.title})`
}
if (this.language.length == 0) {
throw `No languages defined. Define at least one language. (${context}.languages)`
throw `No languages defined. Define at least one language. You can do this by adding a title`
}
if (json.title === undefined) {
throw "Title not defined in " + this.id
@ -201,14 +209,7 @@ export default class LayoutConfig implements LayoutInformation {
this.enableExportButton = json.enableDownload ?? true
this.enablePdfDownload = json.enablePdfDownload ?? true
this.customCss = json.customCss
this.overpassUrl = Constants.defaultOverpassUrls
if (json.overpassUrl !== undefined) {
if (typeof json.overpassUrl === "string") {
this.overpassUrl = [json.overpassUrl]
} else {
this.overpassUrl = json.overpassUrl
}
}
this.overpassUrl = json.overpassUrl ?? Constants.defaultOverpassUrls
this.overpassTimeout = json.overpassTimeout ?? 30
this.overpassMaxZoom = json.overpassMaxZoom ?? 16
this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1