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

@ -2,16 +2,30 @@
"type": "object",
"properties": {
"icon": {
"description": "question: What icon should be shown in the link button?\nifunset: do not show an icon\ntype: icon",
"type": "string"
},
"text": {},
"text": {
"description": "question: What text should be shown in the link icon?\n\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\nifunset: do not show a text",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"href": {
"description": "question: if clicked, what webpage should open?\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\ntype: url",
"type": "string"
},
"newTab": {
"description": "question: Should the link open in a new tab?\niftrue: Open in a new tab\niffalse: do not open in a new tab\nifunset: do not open in a new tab",
"type": "boolean"
},
"requirements": {
"description": "question: When should the extra button be shown?\nsuggestions: 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\"}]",
"type": "array",
"items": {
"enum": [
@ -82,6 +96,10 @@
"or"
],
"additionalProperties": false
},
"Record<string,string>": {
"type": "object",
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#",

View file

@ -2,16 +2,30 @@ export default {
"type": "object",
"properties": {
"icon": {
"description": "question: What icon should be shown in the link button?\nifunset: do not show an icon\ntype: icon",
"type": "string"
},
"text": {},
"text": {
"description": "question: What text should be shown in the link icon?\n\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\nifunset: do not show a text",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"href": {
"description": "question: if clicked, what webpage should open?\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\ntype: url",
"type": "string"
},
"newTab": {
"description": "question: Should the link open in a new tab?\niftrue: Open in a new tab\niffalse: do not open in a new tab\nifunset: do not open in a new tab",
"type": "boolean"
},
"requirements": {
"description": "question: When should the extra button be shown?\nsuggestions: 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\"}]",
"type": "array",
"items": {
"enum": [
@ -80,6 +94,9 @@ export default {
"required": [
"or"
]
},
"Record<string,string>": {
"type": "object"
}
},
"$schema": "http://json-schema.org/draft-07/schema#"

View file

@ -117,6 +117,10 @@
"or"
],
"additionalProperties": false
},
"Record<string,string>": {
"type": "object",
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#",

View file

@ -115,6 +115,9 @@ export default {
"required": [
"or"
]
},
"Record<string,string>": {
"type": "object"
}
},
"$schema": "http://json-schema.org/draft-07/schema#"

View file

@ -3,12 +3,22 @@
"type": "object",
"properties": {
"id": {
"description": "The id of this layout.\n\nThis is used as hashtag in the changeset message, which will read something like \"Adding data with #mapcomplete for theme #<the theme id>\"\nMake sure it is something decent and descriptive, it should be a simple, lowercase string.\n\nOn official themes, it'll become the name of the page, e.g.\n'cyclestreets' which become 'cyclestreets.html'",
"description": "question: What is the id of this layout?\n\nThe id is a unique string to identify the theme\n\nIt should be\n- in english\n- describe the theme in a single word (or a few words)\n- all lowercase and with only [a-z] or underscores (_)\n\nThis is used as hashtag in the changeset message, which will read something like \"Adding data with #mapcomplete for theme #<the theme id>\"\n\nOn official themes, it'll become the name of the page, e.g.\n'cyclestreets' which become 'cyclestreets.html'\n\ntype: id\ngroup: basic",
"type": "string"
},
"credits": {
"description": "Who helped to create this theme and should be attributed?",
"type": "string"
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
},
"mustHaveLanguage": {
"description": "Only used in 'generateLayerOverview': if present, every translation will be checked to make sure it is fully translated.\n\nThis must be a list of two-letter, lowercase codes which identifies the language, e.g. \"en\", \"nl\", ...",
@ -18,7 +28,7 @@
}
},
"title": {
"description": "The title, as shown in the welcome message and the more-screen.",
"description": "question: What is the title of this theme?\n\nThe human-readable title, as shown in the welcome message and the index page\ngroup: basic",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -29,7 +39,7 @@
]
},
"shortDescription": {
"description": "A short description, showed as social description and in the 'more theme'-buttons.\nNote that if this one is not defined, the first sentence of 'description' is used",
"description": "A short description, showed as social description and in the 'more theme'-buttons.\nNote that if this one is not defined, the first sentence of 'description' is used\ngroup: hidden",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -40,7 +50,7 @@
]
},
"description": {
"description": "The description, as shown in the welcome message and the more-screen",
"description": "question: How would you describe this theme?\nThe description, as shown in the welcome message and the more-screen\ngroup: basic",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -51,7 +61,7 @@
]
},
"descriptionTail": {
"description": "A part of the description, shown under the login-button.",
"description": "A part of the description, shown under the login-button.\ngroup: hidden",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -62,21 +72,23 @@
]
},
"icon": {
"description": "The icon representing this theme.\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon",
"description": "question: What icon should be used to represent this theme?\n\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\n\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon\ngroup: basic",
"type": "string"
},
"socialImage": {
"description": "Link to a 'social image' which is included as og:image-tag on official themes.\nUseful to share the theme on social media.\nSee https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information$\n\nType: image",
"description": "question: What image should be used as social image preview?\nThis is included as og:image-tag on official themes.\n\nSee https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information\nifunset: use the default social image of mapcomplete (or generate one based on the icon)\nType: image\ngroup: basic",
"type": "string"
},
"startZoom": {
"description": "Default location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used",
"description": "question: At what zoomlevel should this theme open?\nDefault location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used\nifunset: Use the default startzoom (0)\ntype: float\ngroup: start_location",
"type": "number"
},
"startLat": {
"description": "question: At what start latitude should this theme open?\nDefault location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used\nifunset: Use 0 as start latitude\ntype: float\ngroup: start_location",
"type": "number"
},
"startLon": {
"description": "question: At what start longitude should this theme open?\nDefault location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used\nifunset: Use 0 as start longitude\ntype: float\ngroup: start_location",
"type": "number"
},
"widenFactor": {
@ -119,7 +131,7 @@
}
},
"layers": {
"description": "The layers to display.\n\nEvery layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\n\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\n\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\n\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```",
"description": "question: What layers should this map show?\ntype: layer[]\ntypes: hidden | layer | hidden\ngroup: layers\n\nEvery layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\n\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\n\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\n\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```",
"type": "array",
"items": {
"anyOf": [
@ -165,7 +177,7 @@
}
},
"customCss": {
"description": "The URL of a custom CSS stylesheet to modify the layout",
"description": "The URL of a custom CSS stylesheet to modify the layout\ngroup: advanced",
"type": "string"
},
"hideFromOverview": {
@ -220,77 +232,70 @@
]
},
"extraLink": {
"description": "Adds an additional button on the top-left of the application.\nThis can link to an arbitrary location.\n\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\nDefault: {icon: \"./assets/svg/pop-out.svg\", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: [\"iframe\",\"no-welcome-message]},",
"description": "question: should an extra help button be shown in certain circumstances?\nAdds an additional button on the top-left of the application.\nThis can link to an arbitrary location.\n\nFor example {icon: \"./assets/svg/pop-out.svg\", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: [\"iframe\",\"no-welcome-message]},\n\ngroup: advanced\nifunset: show a link to open MapComplete full screen if used in an iframe",
"$ref": "#/definitions/default"
},
"enableUserBadge": {
"description": "If set to false, disables logging in.\nThe userbadge will be hidden, all login-buttons will be hidden and editing will be disabled",
"description": "question: Should a user be able to login with OpenStreetMap?\n\nIf not logged in, will not show the login buttons and hide all the editable elements.\nAs such, MapComplete will become read-only and a purely visualisation tool.\n\nifunset: Enable the possiblity to login with OpenStreetMap (default)\niffalse: Do not enable to login with OpenStreetMap, have a read-only view of MapComplete.\niftrue: Enable the possiblity to login with OpenStreetMap\ngroup: feature_switches",
"type": "boolean"
},
"enableShareScreen": {
"description": "If false, hides the tab 'share'-tab in the welcomeMessage",
"description": "question: Should the tab with options to share the current screen be enabled?\n\nOn can get the iFrame embed code here\n\nifunset: Enable the sharescreen (default)\niffalse: Do not enable the share screen\niftrue: Enable the share screen\ngroup: feature_switches",
"type": "boolean"
},
"enableMoreQuests": {
"description": "Hides the tab with more themes in the welcomeMessage",
"description": "question: Should the user be able to switch to different themes?\n\nTypically enabled in iframes and/or on commisioned themes\n\niftrue: enable to go back to the index page showing all themes\niffalse: do not enable to go back to the index page showing all themes; hide the 'more themes' buttons\nifunset: mapcomplete default: enable to go back to the index page showing all themes\ngroup: feature_switches",
"type": "boolean"
},
"enableLayers": {
"description": "If false, the layer selection/filter view will be hidden\nThe corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'",
"description": "question: Should the user be able to enable/disable layers and to filter the layers?\n\nThe corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'\niftrue: enable the filters/layers pane\niffalse: do not enable to filter or to disable layers; hide the 'filter' tab from the overview and the button at the bottom-left\nifunset: mapcomplete default: enable to filter or to enable/disable layers\ngroup: feature_switches",
"type": "boolean"
},
"enableSearch": {
"description": "If set to false, hides the search bar",
"description": "question: Should the user be able to search for locations?\n\nifunset: MapComplete default: allow to search\niftrue: Allow to search\niffalse: Do not allow to search; hide the search-bar\ngroup: feature_switches",
"type": "boolean"
},
"enableAddNewPoints": {
"description": "If set to false, the ability to add new points or nodes will be disabled.\nEditing already existing features will still be possible",
"description": "question: Should the user be able to add new points?\n\nAdding new points is only possible if the loaded layers have presets set.\nSome layers do not have presets. If the theme only has layers without presets, then adding new points will not be possible.\n\nifunset: MapComplete default: allow to create new points\niftrue: Allow to create new points\niffalse: Do not allow to create new points, even if the layers in this theme support creating new points\ngroup: feature_switches",
"type": "boolean"
},
"enableGeolocation": {
"description": "If set to false, the 'geolocation'-button will be hidden.",
"description": "question: Should the user be able to use their GPS to geolocate themselfes on the map?\nifunset: MapComplete default: allow to use the GPS\niftrue: Allow to use the GPS\niffalse: Do not allow to use the GPS, hide the geolocation-buttons\ngroup: feature_switches",
"type": "boolean"
},
"enableBackgroundLayerSelection": {
"description": "Enable switching the backgroundlayer.\nIf false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well",
"description": "Enable switching the backgroundlayer.\nIf false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well\n\nquestion: Should the user be able to switch the background layer?\n\niftrue: Allow to switch the background layer\niffalse: Do not allow to switch the background layer\nifunset: MapComplete default: Allow to switch the background layer\ngroup: feature_switches",
"type": "boolean"
},
"enableShowAllQuestions": {
"description": "If set to true, will show _all_ unanswered questions in a popup instead of just the next one",
"description": "question: Should the questions about a feature be presented one by one or all at once?\niftrue: Show all unanswered questions at the same time\niffalse: Show unanswered questions one by one\nifunset: MapComplete default: Use the preference of the user to show questions at the same time or one by one\ngroup: feature_switches",
"type": "boolean"
},
"enableDownload": {
"description": "If set to true, download button for the data will be shown (offers downloading as geojson and csv)",
"description": "question: Should the 'download as CSV'- and 'download as Geojson'-buttons be enabled?\niftrue: Enable the option to download the map as CSV and GeoJson\niffalse: Enable the option to download the map as CSV and GeoJson\nifunset: MapComplete default: Enable the option to download the map as CSV and GeoJson\ngroup: feature_switches",
"type": "boolean"
},
"enablePdfDownload": {
"description": "If set to true, exporting a pdf is enabled",
"description": "question: Should the 'download as PDF'-button be enabled?\niftrue: Enable the option to download the map as PDF\niffalse: Enable the option to download the map as PDF\nifunset: MapComplete default: Enable the option to download the map as PDF\ngroup: feature_switches",
"type": "boolean"
},
"enableNoteImports": {
"description": "If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),\nthese notes will be shown if a relevant layer is present.\n\nDefault is true for official layers and false for unofficial (sideloaded) layers",
"description": "question: Should the 'notes' from OpenStreetMap be loaded and parsed for import helper notes?\nIf true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),\nthese notes will be shown if a relevant layer is present.\n\nifunset: MapComplete default: do not load import notes for sideloaded themes but do load them for official themes\niftrue: Load notes and show import notes\niffalse: Do not load import notes\ngroup: advanced",
"type": "boolean"
},
"overpassUrl": {
"description": "Set one or more overpass URLs to use for this theme..",
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
"description": "question: What overpass-api instance should be used for this layout?\n\nifunset: Use the default, builtin collection of overpass instances\ngroup: advanced",
"type": "array",
"items": {
"type": "string"
}
},
"overpassTimeout": {
"description": "Set a different timeout for overpass queries - in seconds. Default: 30s",
"description": "question: After how much seconds should the overpass-query stop?\nIf a query takes too long, the overpass-server will abort.\nOnce can set the amount of time before overpass gives up here.\nifunset: use the default amount of 30 seconds as timeout\ntype: pnat\ngroup: advanced",
"type": "number"
},
"enableNodeDatabase": {
"description": "Enables tracking of all nodes when data is loaded.\nThis is useful for the 'ImportWay' and 'ConflateWay'-buttons who need this database.\n\nNote: this flag will be automatically set.",
"description": "Enables tracking of all nodes when data is loaded.\nThis is useful for the 'ImportWay' and 'ConflateWay'-buttons who need this database.\n\nNote: this flag will be automatically set and can thus be ignored.\ngroup: hidden",
"type": "boolean"
}
},
@ -2580,16 +2585,30 @@
"type": "object",
"properties": {
"icon": {
"description": "question: What icon should be shown in the link button?\nifunset: do not show an icon\ntype: icon",
"type": "string"
},
"text": {},
"text": {
"description": "question: What text should be shown in the link icon?\n\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\nifunset: do not show a text",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"href": {
"description": "question: if clicked, what webpage should open?\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\ntype: url",
"type": "string"
},
"newTab": {
"description": "question: Should the link open in a new tab?\niftrue: Open in a new tab\niffalse: do not open in a new tab\nifunset: do not open in a new tab",
"type": "boolean"
},
"requirements": {
"description": "question: When should the extra button be shown?\nsuggestions: 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\"}]",
"type": "array",
"items": {
"enum": [

View file

@ -3,12 +3,22 @@ export default {
"type": "object",
"properties": {
"id": {
"description": "The id of this layout.\n\nThis is used as hashtag in the changeset message, which will read something like \"Adding data with #mapcomplete for theme #<the theme id>\"\nMake sure it is something decent and descriptive, it should be a simple, lowercase string.\n\nOn official themes, it'll become the name of the page, e.g.\n'cyclestreets' which become 'cyclestreets.html'",
"description": "question: What is the id of this layout?\n\nThe id is a unique string to identify the theme\n\nIt should be\n- in english\n- describe the theme in a single word (or a few words)\n- all lowercase and with only [a-z] or underscores (_)\n\nThis is used as hashtag in the changeset message, which will read something like \"Adding data with #mapcomplete for theme #<the theme id>\"\n\nOn official themes, it'll become the name of the page, e.g.\n'cyclestreets' which become 'cyclestreets.html'\n\ntype: id\ngroup: basic",
"type": "string"
},
"credits": {
"description": "Who helped to create this theme and should be attributed?",
"type": "string"
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
},
"mustHaveLanguage": {
"description": "Only used in 'generateLayerOverview': if present, every translation will be checked to make sure it is fully translated.\n\nThis must be a list of two-letter, lowercase codes which identifies the language, e.g. \"en\", \"nl\", ...",
@ -18,7 +28,7 @@ export default {
}
},
"title": {
"description": "The title, as shown in the welcome message and the more-screen.",
"description": "question: What is the title of this theme?\n\nThe human-readable title, as shown in the welcome message and the index page\ngroup: basic",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -29,7 +39,7 @@ export default {
]
},
"shortDescription": {
"description": "A short description, showed as social description and in the 'more theme'-buttons.\nNote that if this one is not defined, the first sentence of 'description' is used",
"description": "A short description, showed as social description and in the 'more theme'-buttons.\nNote that if this one is not defined, the first sentence of 'description' is used\ngroup: hidden",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -40,7 +50,7 @@ export default {
]
},
"description": {
"description": "The description, as shown in the welcome message and the more-screen",
"description": "question: How would you describe this theme?\nThe description, as shown in the welcome message and the more-screen\ngroup: basic",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -51,7 +61,7 @@ export default {
]
},
"descriptionTail": {
"description": "A part of the description, shown under the login-button.",
"description": "A part of the description, shown under the login-button.\ngroup: hidden",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
@ -62,21 +72,23 @@ export default {
]
},
"icon": {
"description": "The icon representing this theme.\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon",
"description": "question: What icon should be used to represent this theme?\n\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\n\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon\ngroup: basic",
"type": "string"
},
"socialImage": {
"description": "Link to a 'social image' which is included as og:image-tag on official themes.\nUseful to share the theme on social media.\nSee https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information$\n\nType: image",
"description": "question: What image should be used as social image preview?\nThis is included as og:image-tag on official themes.\n\nSee https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information\nifunset: use the default social image of mapcomplete (or generate one based on the icon)\nType: image\ngroup: basic",
"type": "string"
},
"startZoom": {
"description": "Default location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used",
"description": "question: At what zoomlevel should this theme open?\nDefault location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used\nifunset: Use the default startzoom (0)\ntype: float\ngroup: start_location",
"type": "number"
},
"startLat": {
"description": "question: At what start latitude should this theme open?\nDefault location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used\nifunset: Use 0 as start latitude\ntype: float\ngroup: start_location",
"type": "number"
},
"startLon": {
"description": "question: At what start longitude should this theme open?\nDefault location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used\nifunset: Use 0 as start longitude\ntype: float\ngroup: start_location",
"type": "number"
},
"widenFactor": {
@ -119,7 +131,7 @@ export default {
}
},
"layers": {
"description": "The layers to display.\n\nEvery layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\n\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\n\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\n\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```",
"description": "question: What layers should this map show?\ntype: layer[]\ntypes: hidden | layer | hidden\ngroup: layers\n\nEvery layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\n\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\n\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\n\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```",
"type": "array",
"items": {
"anyOf": [
@ -165,7 +177,7 @@ export default {
}
},
"customCss": {
"description": "The URL of a custom CSS stylesheet to modify the layout",
"description": "The URL of a custom CSS stylesheet to modify the layout\ngroup: advanced",
"type": "string"
},
"hideFromOverview": {
@ -220,77 +232,70 @@ export default {
]
},
"extraLink": {
"description": "Adds an additional button on the top-left of the application.\nThis can link to an arbitrary location.\n\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\nDefault: {icon: \"./assets/svg/pop-out.svg\", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: [\"iframe\",\"no-welcome-message]},",
"description": "question: should an extra help button be shown in certain circumstances?\nAdds an additional button on the top-left of the application.\nThis can link to an arbitrary location.\n\nFor example {icon: \"./assets/svg/pop-out.svg\", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: [\"iframe\",\"no-welcome-message]},\n\ngroup: advanced\nifunset: show a link to open MapComplete full screen if used in an iframe",
"$ref": "#/definitions/default"
},
"enableUserBadge": {
"description": "If set to false, disables logging in.\nThe userbadge will be hidden, all login-buttons will be hidden and editing will be disabled",
"description": "question: Should a user be able to login with OpenStreetMap?\n\nIf not logged in, will not show the login buttons and hide all the editable elements.\nAs such, MapComplete will become read-only and a purely visualisation tool.\n\nifunset: Enable the possiblity to login with OpenStreetMap (default)\niffalse: Do not enable to login with OpenStreetMap, have a read-only view of MapComplete.\niftrue: Enable the possiblity to login with OpenStreetMap\ngroup: feature_switches",
"type": "boolean"
},
"enableShareScreen": {
"description": "If false, hides the tab 'share'-tab in the welcomeMessage",
"description": "question: Should the tab with options to share the current screen be enabled?\n\nOn can get the iFrame embed code here\n\nifunset: Enable the sharescreen (default)\niffalse: Do not enable the share screen\niftrue: Enable the share screen\ngroup: feature_switches",
"type": "boolean"
},
"enableMoreQuests": {
"description": "Hides the tab with more themes in the welcomeMessage",
"description": "question: Should the user be able to switch to different themes?\n\nTypically enabled in iframes and/or on commisioned themes\n\niftrue: enable to go back to the index page showing all themes\niffalse: do not enable to go back to the index page showing all themes; hide the 'more themes' buttons\nifunset: mapcomplete default: enable to go back to the index page showing all themes\ngroup: feature_switches",
"type": "boolean"
},
"enableLayers": {
"description": "If false, the layer selection/filter view will be hidden\nThe corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'",
"description": "question: Should the user be able to enable/disable layers and to filter the layers?\n\nThe corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'\niftrue: enable the filters/layers pane\niffalse: do not enable to filter or to disable layers; hide the 'filter' tab from the overview and the button at the bottom-left\nifunset: mapcomplete default: enable to filter or to enable/disable layers\ngroup: feature_switches",
"type": "boolean"
},
"enableSearch": {
"description": "If set to false, hides the search bar",
"description": "question: Should the user be able to search for locations?\n\nifunset: MapComplete default: allow to search\niftrue: Allow to search\niffalse: Do not allow to search; hide the search-bar\ngroup: feature_switches",
"type": "boolean"
},
"enableAddNewPoints": {
"description": "If set to false, the ability to add new points or nodes will be disabled.\nEditing already existing features will still be possible",
"description": "question: Should the user be able to add new points?\n\nAdding new points is only possible if the loaded layers have presets set.\nSome layers do not have presets. If the theme only has layers without presets, then adding new points will not be possible.\n\nifunset: MapComplete default: allow to create new points\niftrue: Allow to create new points\niffalse: Do not allow to create new points, even if the layers in this theme support creating new points\ngroup: feature_switches",
"type": "boolean"
},
"enableGeolocation": {
"description": "If set to false, the 'geolocation'-button will be hidden.",
"description": "question: Should the user be able to use their GPS to geolocate themselfes on the map?\nifunset: MapComplete default: allow to use the GPS\niftrue: Allow to use the GPS\niffalse: Do not allow to use the GPS, hide the geolocation-buttons\ngroup: feature_switches",
"type": "boolean"
},
"enableBackgroundLayerSelection": {
"description": "Enable switching the backgroundlayer.\nIf false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well",
"description": "Enable switching the backgroundlayer.\nIf false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well\n\nquestion: Should the user be able to switch the background layer?\n\niftrue: Allow to switch the background layer\niffalse: Do not allow to switch the background layer\nifunset: MapComplete default: Allow to switch the background layer\ngroup: feature_switches",
"type": "boolean"
},
"enableShowAllQuestions": {
"description": "If set to true, will show _all_ unanswered questions in a popup instead of just the next one",
"description": "question: Should the questions about a feature be presented one by one or all at once?\niftrue: Show all unanswered questions at the same time\niffalse: Show unanswered questions one by one\nifunset: MapComplete default: Use the preference of the user to show questions at the same time or one by one\ngroup: feature_switches",
"type": "boolean"
},
"enableDownload": {
"description": "If set to true, download button for the data will be shown (offers downloading as geojson and csv)",
"description": "question: Should the 'download as CSV'- and 'download as Geojson'-buttons be enabled?\niftrue: Enable the option to download the map as CSV and GeoJson\niffalse: Enable the option to download the map as CSV and GeoJson\nifunset: MapComplete default: Enable the option to download the map as CSV and GeoJson\ngroup: feature_switches",
"type": "boolean"
},
"enablePdfDownload": {
"description": "If set to true, exporting a pdf is enabled",
"description": "question: Should the 'download as PDF'-button be enabled?\niftrue: Enable the option to download the map as PDF\niffalse: Enable the option to download the map as PDF\nifunset: MapComplete default: Enable the option to download the map as PDF\ngroup: feature_switches",
"type": "boolean"
},
"enableNoteImports": {
"description": "If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),\nthese notes will be shown if a relevant layer is present.\n\nDefault is true for official layers and false for unofficial (sideloaded) layers",
"description": "question: Should the 'notes' from OpenStreetMap be loaded and parsed for import helper notes?\nIf true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),\nthese notes will be shown if a relevant layer is present.\n\nifunset: MapComplete default: do not load import notes for sideloaded themes but do load them for official themes\niftrue: Load notes and show import notes\niffalse: Do not load import notes\ngroup: advanced",
"type": "boolean"
},
"overpassUrl": {
"description": "Set one or more overpass URLs to use for this theme..",
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
"description": "question: What overpass-api instance should be used for this layout?\n\nifunset: Use the default, builtin collection of overpass instances\ngroup: advanced",
"type": "array",
"items": {
"type": "string"
}
},
"overpassTimeout": {
"description": "Set a different timeout for overpass queries - in seconds. Default: 30s",
"description": "question: After how much seconds should the overpass-query stop?\nIf a query takes too long, the overpass-server will abort.\nOnce can set the amount of time before overpass gives up here.\nifunset: use the default amount of 30 seconds as timeout\ntype: pnat\ngroup: advanced",
"type": "number"
},
"enableNodeDatabase": {
"description": "Enables tracking of all nodes when data is loaded.\nThis is useful for the 'ImportWay' and 'ConflateWay'-buttons who need this database.\n\nNote: this flag will be automatically set.",
"description": "Enables tracking of all nodes when data is loaded.\nThis is useful for the 'ImportWay' and 'ConflateWay'-buttons who need this database.\n\nNote: this flag will be automatically set and can thus be ignored.\ngroup: hidden",
"type": "boolean"
}
},
@ -2556,16 +2561,30 @@ export default {
"type": "object",
"properties": {
"icon": {
"description": "question: What icon should be shown in the link button?\nifunset: do not show an icon\ntype: icon",
"type": "string"
},
"text": {},
"text": {
"description": "question: What text should be shown in the link icon?\n\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\nifunset: do not show a text",
"anyOf": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
]
},
"href": {
"description": "question: if clicked, what webpage should open?\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced\n\ntype: url",
"type": "string"
},
"newTab": {
"description": "question: Should the link open in a new tab?\niftrue: Open in a new tab\niffalse: do not open in a new tab\nifunset: do not open in a new tab",
"type": "boolean"
},
"requirements": {
"description": "question: When should the extra button be shown?\nsuggestions: 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\"}]",
"type": "array",
"items": {
"enum": [

View file

@ -83,6 +83,10 @@
"or"
],
"additionalProperties": false
},
"Record<string,string>": {
"type": "object",
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#",

View file

@ -81,6 +81,9 @@ export default {
"required": [
"or"
]
},
"Record<string,string>": {
"type": "object"
}
},
"$schema": "http://json-schema.org/draft-07/schema#"

View file

@ -41,4 +41,4 @@
"layers": [
"drinking_water"
]
}
}

View file

@ -785,6 +785,10 @@ video {
margin: 0.5rem;
}
.m-4 {
margin: 1rem;
}
.m-1 {
margin: 0.25rem;
}
@ -797,10 +801,6 @@ video {
margin: 0px;
}
.m-4 {
margin: 1rem;
}
.m-3 {
margin: 0.75rem;
}
@ -874,10 +874,6 @@ video {
margin-right: 1.5rem;
}
.mr-1 {
margin-right: 0.25rem;
}
.mt-1 {
margin-top: 0.25rem;
}
@ -890,6 +886,10 @@ video {
margin-right: 0px;
}
.mr-1 {
margin-right: 0.25rem;
}
.ml-1 {
margin-left: 0.25rem;
}
@ -1071,10 +1071,6 @@ video {
height: 1.5rem;
}
.h-4 {
height: 1rem;
}
.h-12 {
height: 3rem;
}
@ -1085,6 +1081,10 @@ video {
height: fit-content;
}
.h-4 {
height: 1rem;
}
.h-1\/2 {
height: 50%;
}
@ -1169,10 +1169,6 @@ video {
width: 1.5rem;
}
.w-4 {
width: 1rem;
}
.w-12 {
width: 3rem;
}
@ -1191,6 +1187,10 @@ video {
width: 6rem;
}
.w-4 {
width: 1rem;
}
.w-0 {
width: 0px;
}
@ -1696,14 +1696,14 @@ video {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.p-1 {
padding: 0.25rem;
}
.p-4 {
padding: 1rem;
}
.p-0\.5 {
padding: 0.125rem;
}

View file

@ -100,7 +100,7 @@ export default class ScriptUtils {
.filter((path) => path.indexOf("license_info.json") < 0)
}
public static getThemeFiles(): { parsed: LayoutConfigJson; path: string }[] {
public static getThemeFiles(): { parsed: LayoutConfigJson; path: string; raw: string }[] {
return this.getThemePaths().map((path) => {
try {
const contents = readFileSync(path, { encoding: "utf8" })
@ -108,7 +108,7 @@ export default class ScriptUtils {
throw "The file " + path + " is empty, did you properly save?"
}
const parsed = JSON.parse(contents)
return { parsed: parsed, path: path }
return { parsed: parsed, path: path, raw: contents }
} catch (e) {
console.error("Could not read file ", path, "due to ", e)
throw e

View file

@ -13,8 +13,8 @@ mkdir dist/assets 2> /dev/null
export NODE_OPTIONS="--max-old-space-size=8192"
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
npm run generate:editor-layer-index &&
npm run generate &&
# npm run generate:editor-layer-index &&
# npm run generate &&
npm run generate:layouts
if [ $? -ne 0 ]; then
@ -24,7 +24,7 @@ fi
SRC_MAPS=""
SRC_MAPS="--sourcemap"
BRANCH=`git rev-parse --abbrev-ref HEAD`
echo "The branch name is $BRANCH"
if [ $BRANCH = "develop" ]
@ -46,6 +46,7 @@ else
fi
export NODE_OPTIONS=--max-old-space-size=7000
which vite
vite build $SRC_MAPS
# Copy the layer files, as these might contain assets (e.g. svgs)
cp -r assets/layers/ dist/assets/layers/

View file

@ -141,7 +141,8 @@ function extractHintsFrom(
description: string,
fieldnames: string[],
path: (string | number)[],
type: any
type: any,
schemepart: any
): Record<string, string> {
if (!description) {
return {}
@ -167,19 +168,28 @@ function extractHintsFrom(
}
if (hints["types"]) {
const notRequired = hints["ifunset"] !== undefined
const numberOfExpectedSubtypes = hints["types"].replaceAll("|", ";").split(";").length
if (!Array.isArray(type)) {
if (!Array.isArray(type) && !notRequired) {
throw (
"At " +
path.join(".") +
"Invalid hint in the documentation: `types` indicates that there are " +
numberOfExpectedSubtypes +
" subtypes, but object does not support subtypes. Did you mean `type` instead?\n\tTypes are: " +
hints["types"]
hints["types"] +
"\n: hints: " +
JSON.stringify(hints) +
" req:" +
JSON.stringify(schemepart)
)
}
const numberOfActualTypes = type.length
if (numberOfActualTypes !== numberOfExpectedSubtypes) {
if (
numberOfActualTypes !== numberOfExpectedSubtypes &&
notRequired &&
numberOfActualTypes + 1 !== numberOfExpectedSubtypes
) {
throw `At ${path.join(
"."
)}\nInvalid hint in the documentation: \`types\` indicates that there are ${numberOfExpectedSubtypes} subtypes, but there are ${numberOfActualTypes} subtypes
@ -213,10 +223,16 @@ function addMetafields(fieldnames: string[], fullSchema: JsonSchema): ConfigMeta
const type = schemePart.items?.anyOf ?? schemePart.type ?? schemePart.anyOf
let description = schemePart.description
let hints = extractHintsFrom(description, fieldnames, path, type)
let hints = extractHintsFrom(description, fieldnames, path, type, schemePart)
const childDescription = schemePart["child-description"]
if (childDescription) {
const childHints = extractHintsFrom(childDescription, fieldnames, path, type)
const childHints = extractHintsFrom(
childDescription,
fieldnames,
path,
type,
schemePart
)
hints = { ...childHints, ...hints }
description = description ?? childDescription
}
@ -299,9 +315,9 @@ function validateMeta(path: ConfigMeta): string | undefined {
}
if (path.hints.question === undefined && !Array.isArray(path.type)) {
/* return (
ctx +
" does not have a question set. As such, MapComplete-studio users will not be able to set this property"
) //*/
ctx +
" does not have a question set. As such, MapComplete-studio users will not be able to set this property"
) //*/
}
return undefined

View file

@ -7,6 +7,7 @@ import {
import Translations from "../src/UI/i18n/Translations"
import { Translation } from "../src/UI/i18n/Translation"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/Conversion"
/*
* This script reads all theme and layer files and reformats them inplace
@ -68,7 +69,7 @@ for (const layerFile of layerFiles) {
const fixed = <LayerConfigJson>(
new UpdateLegacyLayer().convertStrict(
layerFile.parsed,
"While linting " + layerFile.path
ConversionContext.construct([layerFile.path.split("/").at(-1)], ["update legacy"])
)
)
addArticleToPresets(fixed)
@ -83,7 +84,7 @@ for (const themeFile of themeFiles) {
try {
const fixed = new FixLegacyTheme().convertStrict(
themeFile.parsed,
"While linting " + themeFile.path
ConversionContext.construct([themeFile.path.split("/").at(-1)], ["update legacy layer"])
)
for (const layer of fixed.layers) {
if (layer["presets"] !== undefined) {
@ -91,7 +92,11 @@ for (const themeFile of themeFiles) {
}
}
// extractInlineLayer(fixed)
writeFileSync(themeFile.path, JSON.stringify(fixed, null, " "))
const endsWithNewline = themeFile.raw.at(-1) === "\n"
writeFileSync(
themeFile.path,
JSON.stringify(fixed, null, " ") + (endsWithNewline ? "\n" : "")
)
} catch (e) {
console.error("COULD NOT LINT THEME" + themeFile.path + ":\n\t" + e)
}

View file

@ -2,6 +2,7 @@ import * as fs from "node:fs"
import * as http from "node:http"
import * as path from "node:path"
import ScriptUtils from "./ScriptUtils"
import * as meta from "../package.json"
const PORT = 1235
const CORS = "http://localhost:1234,https://mapcomplete.org,https://dev.mapcomplete.org"
@ -115,4 +116,8 @@ http.createServer(async (req, res) => {
}
}).listen(PORT)
console.log(`Server running at http://127.0.0.1:${PORT}/`)
console.log(
`Server started at http://127.0.0.1:${PORT}/, the time is ${new Date().toISOString()}, version from package.json is ${
meta.version
}`
)

View file

@ -83,6 +83,7 @@ export class OsmConnection {
if (options.fakeUser) {
const ud = this.userDetails.data
ud.csCount = 5678
ud.uid = 42
ud.loggedIn = true
ud.unreadMessages = 0
ud.name = "Fake user"

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

View file

@ -41,5 +41,5 @@
<slot />
{:else if $loadingStatus === "not-attempted"}
<slot name="not-logged-in" />
{/if}
{/if}
{/if}

View file

@ -1,35 +1,37 @@
<script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import {Store, UIEventSource} from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import { onDestroy } from "svelte"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../../Utils"
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
import type { Feature } from "geojson";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
import { onDestroy } from "svelte";
import Tr from "../../Base/Tr.svelte";
import Translations from "../../i18n/Translations.js";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import { Utils } from "../../../Utils";
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature | undefined
export let state: SpecialVisualizationState
export let layer: LayerConfig = undefined
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature | undefined;
export let state: SpecialVisualizationState;
export let layer: LayerConfig = undefined;
export let editingEnabled : Store<boolean> | undefined = state?.featureSwitchUserbadge
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge;
export let highlightedRendering: UIEventSource<string> = undefined
export let showQuestionIfUnknown: boolean = false
let editMode = false
onDestroy(
tags.addCallbackAndRunD((tags) => {
editMode = showQuestionIfUnknown && !config.IsKnown(tags)
})
)
export let highlightedRendering: UIEventSource<string> = undefined;
export let showQuestionIfUnknown: boolean = false;
let editMode = false;
if (tags) {
onDestroy(
tags.addCallbackAndRunD((tags) => {
editMode = showQuestionIfUnknown && !config.IsKnown(tags);
})
);
}
let htmlElem: HTMLDivElement
let htmlElem: HTMLDivElement;
$: {
if (editMode && htmlElem !== undefined) {
// EditMode switched to true, so the person wants to make a change
@ -37,32 +39,32 @@
// Some delay is applied to give Svelte the time to render the _question_
window.setTimeout(() => {
Utils.scrollIntoView(<any> htmlElem)
}, 50)
Utils.scrollIntoView(<any>htmlElem);
}, 50);
}
}
const _htmlElement = new UIEventSource<HTMLElement>(undefined)
$: _htmlElement.setData(htmlElem)
const _htmlElement = new UIEventSource<HTMLElement>(undefined);
$: _htmlElement.setData(htmlElem);
function setHighlighting() {
if (highlightedRendering === undefined) {
return
return;
}
if (htmlElem === undefined) {
return
return;
}
const highlighted = highlightedRendering.data
const highlighted = highlightedRendering.data;
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow")
htmlElem.classList.add("glowing-shadow");
} else {
htmlElem.classList.remove("glowing-shadow")
htmlElem.classList.remove("glowing-shadow");
}
}
if (highlightedRendering) {
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()));
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()));
}
</script>

View file

@ -170,7 +170,7 @@
let featureSwitchIsDebugging = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false);
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined);
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0;
if (state) {
if (state?.osmConnection) {
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount;

View file

@ -22,7 +22,7 @@
<slot name="title" />
<div class="flex flex-wrap">
{#each Array.from(layerIds) as layer}
<NextButton clss="small" on:click={() => dispatch("layerSelected", layer.id)}>
<NextButton clss="small" on:click={() => dispatch("layerSelected", layer)}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(layer.id)} />
</div>

View file

@ -209,6 +209,7 @@ export abstract class EditJsonState<T> {
try {
prepare.convert(<T>config, context)
} catch (e) {
console.error(e)
context.err(e)
}
return context.messages

View file

@ -8,7 +8,7 @@
import Region from "./Region.svelte";
export let state: EditThemeState;
let schema: ConfigMeta[] = state.schema.filter(schema => schema.path.length > 0 && schema.path[0] !== "layers");
let schema: ConfigMeta[] = state.schema.filter(schema => schema.path.length > 0);
let config = state.configuration;
const messages = state.messages;
const hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
@ -18,47 +18,61 @@
const perRegion: Record<string, ConfigMeta[]> = {};
for (const schemaElement of schema) {
const key = schemaElement.hints.group ?? "no-group"
const list = perRegion[key] ?? (perRegion[key] = [])
list.push(schemaElement)
const key = schemaElement.hints.group ?? "no-group";
const list = perRegion[key] ?? (perRegion[key] = []);
list.push(schemaElement);
}
console.log({perRegion, schema})
</script>
<div class="flex flex-col h-screen">
<div class="w-full flex justify-between my-2">
<slot />
<h3>Editing theme {$title}</h3>
{#if $hasErrors > 0}
<div class="alert">{$hasErrors} errors detected</div>
{:else}
<a class="primary button" href={baseUrl+state.server.urlFor($title, "themes")} target="_blank" rel="noopener">
Try it out
<ChevronRightIcon class="h-6 w-6 shrink-0" />
</a>
{/if}
</div>
<div class="w-full flex justify-between my-2">
<slot />
<h3>Editing theme {$title}</h3>
{#if $hasErrors > 0}
<div class="alert">{$hasErrors} errors detected</div>
{:else}
<a class="primary button" href={baseUrl+state.server.urlFor($title, "themes")} target="_blank" rel="noopener">
Try it out
<ChevronRightIcon class="h-6 w-6 shrink-0" />
</a>
{/if}
</div>
<div class="m4 h-full overflow-y-auto">
{Object.keys(perRegion).join(";")}
<TabbedGroup>
<div slot="title0">Basic properties</div>
<div slot="content0">
<Region {state} configs={perRegion["basic"]} path={[]}></Region>
<Region {state} configs={perRegion["no-group"]} path={[]}></Region>
</div>
<div slot="title1">Feature switches</div>
<div slot="content1">
<Region {state} configs={perRegion["feature_switches"]} path={[]}></Region>
</div>
<div slot="title2">Configuration file</div>
<div slot="content2">
<div class="literal-code">
{JSON.stringify($config)}
<div class="m4 h-full overflow-y-auto">
{Object.keys(perRegion).join(";")}
<TabbedGroup>
<div slot="title0">Basic properties</div>
<div slot="content0">
<Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties"/>
<Region configs={perRegion["start_location"]} path={[]} {state} title="Start location"/>
</div>
<div slot="title1">Layers</div>
<div slot="content1">
<Region configs={perRegion["layers"]} path={[]} {state} />
</div>
<div slot="title2">Feature switches</div>
<div slot="content2">
<Region configs={perRegion["feature_switches"]} path={[]} {state}></Region>
</div>
<ShowConversionMessages messages={$messages}></ShowConversionMessages>
<div slot="title3">Advanced options</div>
<div slot="content3">
<Region configs={perRegion["advanced"]} path={[]} {state}></Region>
</div>
<div slot="title4">Configuration file</div>
<div slot="content4">
<div class="literal-code">
{JSON.stringify($config)}
</div>
<ShowConversionMessages messages={$messages}></ShowConversionMessages>
</div>
</TabbedGroup>
</div>
</div>
</TabbedGroup>
</div>

View file

@ -7,6 +7,7 @@
import { TrashIcon } from "@babeard/svelte-heroicons/mini";
import QuestionPreview from "./QuestionPreview.svelte";
import { Utils } from "../../Utils";
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte";
export let state: EditLayerState;
export let schema: ConfigMeta;
@ -105,6 +106,12 @@
} while (currentIndex !== targetIndex);
}
function schemaForMultitype() {
const sch = {...schema}
sch.hints.typehint = undefined
return sch
}
</script>
<div class="pl-2">
@ -168,6 +175,8 @@
{/if}
</QuestionPreview>
{:else if schema.hints.types}
<SchemaBasedMultiType {state} path={fusePath(value, [])} schema={schemaForMultitype()}/>
{:else}
{#each subparts as subpart}
<SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={subpart} />

View file

@ -11,7 +11,7 @@
export let path: (string | number)[] = [];
</script>
{#if schema.hints.typehint === "tagrendering[]"}
{#if schema.hints?.typehint?.endsWith("[]")}
<!-- We cheat a bit here by matching this 'magical' type... -->
<SchemaBasedArray {path} {state} {schema} />
{:else if schema.type === "array" && schema.hints.multianswer === "true"}

View file

@ -89,6 +89,8 @@
const existingValue = state.getCurrentValueFor(path);
let hasOverride = existingValue?.override !== undefined;
console.log({existingValue, hasOverride})
if (hasBooleanOption >= 0 && (existingValue === true || existingValue === false)) {
tags.setData({ value: "" + existingValue });
} else if (lastIsString && typeof existingValue === "string") {
@ -200,18 +202,23 @@
<h3>{schema.hints.title}</h3>
<div> {schema.description} </div>
{/if}
<div>
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
</div>
{#if hasOverride}
This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited with MapComplete
Studio
{:else}
<div>
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
</div>
{#if chosenOption !== undefined}
{#each subSchemas as subschema}
<SchemaBasedInput {state} schema={subschema}
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
{/each}
{:else if $messages.length > 0}
{#each $messages as msg}
<div class="alert">{msg.message}</div>
{/each}
{#if chosenOption !== undefined}
{#each subSchemas as subschema}
<SchemaBasedInput {state} schema={subschema}
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
{/each}
{:else if $messages.length > 0}
{#each $messages as msg}
<div class="alert">{msg.message}</div>
{/each}
{/if}
{/if}
</div>

View file

@ -20,12 +20,8 @@ export default class StudioServer {
}[]
> {
const uid = this._userId.data
let uidQueryParam = ""
if (this._userId.data !== undefined) {
uidQueryParam = "?userId=" + uid
}
const { allFiles } = <{ allFiles: string[] }>(
await Utils.downloadJson(this.url + "/overview" + uidQueryParam)
await Utils.downloadJson(this.url + "/overview")
)
const layerOverview: {
id: string
@ -33,12 +29,15 @@ export default class StudioServer {
category: "layers" | "themes"
}[] = []
for (let file of allFiles) {
let owner = undefined
if (file.startsWith("" + uid)) {
owner = uid
let parts = file.split("/")
let owner = Number(parts[0])
if (!isNaN(owner)) {
parts.splice(0, 1)
file = file.substring(file.indexOf("/") + 1)
} else {
owner = undefined
}
const category = <"layers" | "themes">file.substring(0, file.indexOf("/"))
const category = <"layers" | "themes">parts[0]
const id = file.substring(file.lastIndexOf("/") + 1, file.length - ".json".length)
if (Constants.priviliged_layers.indexOf(<any>id) > 0) {
continue
@ -48,9 +47,13 @@ export default class StudioServer {
return layerOverview
}
async fetch(layerId: string, category: "layers" | "themes"): Promise<LayerConfigJson> {
async fetch(
layerId: string,
category: "layers" | "themes",
uid?: number
): Promise<LayerConfigJson> {
try {
return await Utils.downloadJson(this.urlFor(layerId, category))
return await Utils.downloadJson(this.urlFor(layerId, category, uid))
} catch (e) {
return undefined
}
@ -73,8 +76,8 @@ export default class StudioServer {
return this.urlFor(id, "layers")
}
public urlFor(id: string, category: "layers" | "themes") {
const uid = this._userId.data
public urlFor(id: string, category: "layers" | "themes", uid?: number) {
uid ??= this._userId.data
const uidStr = uid !== undefined ? "/" + uid : ""
return `${this.url}${uidStr}/${category}/${id}/${id}.json`
}

View file

@ -3,7 +3,7 @@
import NextButton from "./Base/NextButton.svelte";
import { Store, UIEventSource } from "../Logic/UIEventSource";
import EditLayerState from "./Studio/EditLayerState";
import EditLayerState, { EditThemeState } from "./Studio/EditLayerState";
import EditLayer from "./Studio/EditLayer.svelte";
import Loading from "../assets/svg/Loading.svelte";
import StudioServer from "./Studio/StudioServer";
@ -12,6 +12,8 @@
import { QueryParameters } from "../Logic/Web/QueryParameters";
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json";
import layoutSchemaRaw from "../../src/assets/schemas/layoutconfigmeta.json";
import If from "./Base/If.svelte";
import BackButton from "./Base/BackButton.svelte";
import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte";
@ -21,6 +23,7 @@
import * as intro from "../assets/studio_introduction.json";
import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini";
import type { ConfigMeta } from "./Studio/configMeta";
import EditTheme from "./Studio/EditTheme.svelte";
export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
@ -35,32 +38,47 @@
const uid = osmConnection.userDetails.map(ud => ud?.uid);
const studio = new StudioServer(studioUrl, uid);
let layersWithErr = uid.bind(uid => UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview()));
let layers: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success);
let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchOverview());
let layers: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success?.filter(l => l.category === "layers"));
let selfLayers = layers.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]);
let otherLayers = layers.mapD(ls => ls.filter(l => l.owner !== uid.data), [uid]);
let otherLayers = layers.mapD(ls => ls.filter(l => l.owner !== undefined && l.owner !== uid.data), [uid]);
let officialLayers = layers.mapD(ls => ls.filter(l => l.owner === undefined), [uid]);
let themes: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success?.filter(l => l.category === "themes"));
let selfThemes = themes.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]);
let otherThemes = themes.mapD(ls => ls.filter(l => l.owner !== undefined && l.owner !== uid.data), [uid]);
let officialThemes = themes.mapD(ls => ls.filter(l => l.owner === undefined), [uid]);
let state: undefined | "edit_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
let initialLayerConfig: { id: string };
let state: undefined | "edit_layer" | "edit_theme" | "editing_layer" | "editing_theme" | "loading" = undefined;
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
let editLayerState = new EditLayerState(layerSchema, studio, osmConnection);
const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw;
let editThemeState = new EditThemeState(layoutSchema, studio);
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true"));
async function editLayer(event: Event) {
const layerId = event.detail;
const layerId: {owner: number, id: string} = event.detail;
state = "loading";
initialLayerConfig = await studio.fetchLayer(layerId);
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner));
state = "editing_layer";
}
async function editTheme(event: Event) {
const id : {id: string, owner: number} = event.detail;
state = "loading";
editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner));
state = "editing_theme";
}
async function createNewLayer() {
state = "loading";
initialLayerConfig = {
const initialLayerConfig = {
credits: createdBy,
minzoom: 15,
pointRendering: [
@ -77,6 +95,7 @@
color: "blue"
}]
};
editLayerState.configuration.setData(initialLayerConfig);
state = "editing_layer";
}
@ -123,14 +142,12 @@
<NextButton on:click={() => createNewLayer()}>
Create a new layer
</NextButton>
<!--
<NextButton on:click={() => state = "edit_theme"}>
Edit a theme
</NextButton>
<NextButton on:click={() => state = "new_theme"}>
<NextButton on:click={() => {editThemeState.configuration.setData({}); state = "editing_theme"}}>
Create a new theme
</NextButton>
-->
<NextButton clss="small" on:click={() => {showIntro.setData(true)} }>
<QuestionMarkCircleIcon class="w-6 h-6" />
Show the introduction again
@ -146,25 +163,48 @@
<ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}>
<h3 slot="title">Your layers</h3>
</ChooseLayerToEdit>
<h3>Official layers</h3>
<h3>Layers by other contributors</h3>
<ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} />
<h3>Official layers by MapComplete</h3>
<ChooseLayerToEdit layerIds={$officialLayers} on:layerSelected={editLayer} />
</div>
{:else if state === "edit_theme"}
<div class="flex flex-col m-4">
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
<h2>Choose a theme to edit</h2>
<ChooseLayerToEdit layerIds={$selfThemes} on:layerSelected={editTheme}>
<h3 slot="title">Your themes</h3>
</ChooseLayerToEdit>
<h3>Themes by other contributors</h3>
<ChooseLayerToEdit layerIds={$otherThemes} on:layerSelected={editTheme} />
<h3>Official themes by MapComplete</h3>
<ChooseLayerToEdit layerIds={$officialThemes} on:layerSelected={editTheme} />
</div>
{:else if state === "loading"}
<div class="w-8 h-8">
<Loading />
</div>
{:else if state === "editing_layer"}
<EditLayer {initialLayerConfig} state={editLayerState}>
<EditLayer state={editLayerState}>
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
</EditLayer>
{:else if state === "editing_theme"}
<EditTheme state={editThemeState} >
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
</EditTheme>
{/if}
</LoginToggle>
</If>
{#if $showIntro}
<FloatOver>
<FloatOver on:close={() => {showIntro.setData(false)}}>
<div class="flex p-4 h-full">
<Walkthrough pages={intro.sections} on:done={() => {showIntro.setData(false)}} />
</div>

View file

@ -11,9 +11,13 @@
"id"
],
"required": true,
"hints": {},
"hints": {
"typehint": "id",
"group": "basic",
"question": "What is the id of this layout?"
},
"type": "string",
"description": "The id of this layout.\nThis is used as hashtag in the changeset message, which will read something like \"Adding data with #mapcomplete for theme #<the theme id>\"\nMake sure it is something decent and descriptive, it should be a simple, lowercase string.\nOn official themes, it'll become the name of the page, e.g.\n'cyclestreets' which become 'cyclestreets.html'"
"description": "The id is a unique string to identify the theme\nIt should be\n- in english\n- describe the theme in a single word (or a few words)\n- all lowercase and with only [a-z] or underscores (_)\nThis is used as hashtag in the changeset message, which will read something like \"Adding data with #mapcomplete for theme #<the theme id>\"\nOn official themes, it'll become the name of the page, e.g.\n'cyclestreets' which become 'cyclestreets.html'"
},
{
"path": [
@ -21,7 +25,17 @@
],
"required": false,
"hints": {},
"type": "string",
"type": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Who helped to create this theme and should be attributed?"
},
{
@ -38,7 +52,10 @@
"title"
],
"required": true,
"hints": {},
"hints": {
"group": "basic",
"question": "What is the title of this theme?"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
@ -47,14 +64,16 @@
"type": "string"
}
],
"description": "The title, as shown in the welcome message and the more-screen."
"description": "The human-readable title, as shown in the welcome message and the index page"
},
{
"path": [
"shortDescription"
],
"required": false,
"hints": {},
"hints": {
"group": "hidden"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
@ -70,7 +89,10 @@
"description"
],
"required": true,
"hints": {},
"hints": {
"group": "basic",
"question": "How would you describe this theme?"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
@ -86,7 +108,9 @@
"descriptionTail"
],
"required": false,
"hints": {},
"hints": {
"group": "hidden"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
@ -103,10 +127,12 @@
],
"required": true,
"hints": {
"typehint": "icon"
"typehint": "icon",
"group": "basic",
"question": "What icon should be used to represent this theme?"
},
"type": "string",
"description": "The icon representing this theme.\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)"
"description": "Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)"
},
{
"path": [
@ -114,17 +140,53 @@
],
"required": false,
"hints": {
"typehint": "image"
"typehint": "image",
"group": "basic",
"question": "What image should be used as social image preview?",
"ifunset": "use the default social image of mapcomplete (or generate one based on the icon)"
},
"type": "string",
"description": "Link to a 'social image' which is included as og:image-tag on official themes.\nUseful to share the theme on social media.\nSee https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information$"
"description": "This is included as og:image-tag on official themes.\nSee https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information"
},
{
"path": [
"startZoom"
],
"required": true,
"hints": {},
"hints": {
"typehint": "float",
"group": "start_location",
"question": "At what zoomlevel should this theme open?",
"ifunset": "Use the default startzoom (0)"
},
"type": "number",
"description": "Default location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used"
},
{
"path": [
"startLat"
],
"required": true,
"hints": {
"typehint": "float",
"group": "start_location",
"question": "At what start latitude should this theme open?",
"ifunset": "Use 0 as start latitude"
},
"type": "number",
"description": "Default location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used"
},
{
"path": [
"startLon"
],
"required": true,
"hints": {
"typehint": "float",
"group": "start_location",
"question": "At what start longitude should this theme open?",
"ifunset": "Use 0 as start longitude"
},
"type": "number",
"description": "Default location and zoom to start.\nNote that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used"
},
@ -206,7 +268,12 @@
"layers"
],
"required": true,
"hints": {},
"hints": {
"typehint": "layer[]",
"types": "hidden | layer | hidden",
"group": "layers",
"question": "What layers should this map show?"
},
"type": [
{
"description": "Configuration for a single layer",
@ -1102,7 +1169,7 @@
"type": "string"
}
],
"description": "The layers to display.\nEvery layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```"
"description": "Every layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```"
},
{
"path": [
@ -34790,7 +34857,9 @@
"customCss"
],
"required": false,
"hints": {},
"hints": {
"group": "advanced"
},
"type": "string",
"description": "The URL of a custom CSS stylesheet to modify the layout"
},
@ -34861,80 +34930,222 @@
],
"required": false,
"hints": {
"default": "{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",
"question": "should an extra help button be shown in certain circumstances?",
"ifunset": "show a link to open MapComplete full screen if used in an iframe"
},
"type": "object",
"description": "Adds an additional button on the top-left of the application.\nThis can link to an arbitrary location.\nNote that {lat},{lon},{zoom}, {language} and {theme} will be replaced"
"description": "Adds an additional button on the top-left of the application.\nThis can link to an arbitrary location.\nFor example {icon: \"./assets/svg/pop-out.svg\", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: [\"iframe\",\"no-welcome-message]},"
},
{
"path": [
"extraLink",
"icon"
],
"required": false,
"hints": {
"typehint": "icon",
"question": "What icon should be shown in the link button?",
"ifunset": "do not show an icon"
},
"type": "string",
"description": ""
},
{
"path": [
"extraLink",
"text"
],
"required": false,
"hints": {
"question": "What text should be shown in the link icon?",
"ifunset": "do not show a text"
},
"type": [
{
"$ref": "#/definitions/Record<string,string>"
},
{
"type": "string"
}
],
"description": "Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced"
},
{
"path": [
"extraLink",
"href"
],
"required": true,
"hints": {
"typehint": "url",
"question": "if clicked, what webpage should open?"
},
"type": "string",
"description": "Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced"
},
{
"path": [
"extraLink",
"newTab"
],
"required": false,
"hints": {
"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"
},
"type": "boolean",
"description": ""
},
{
"path": [
"extraLink",
"requirements"
],
"required": false,
"hints": {
"question": "When should the extra button be shown?",
"suggestions": [
{
"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"
}
]
},
"type": "array",
"description": ""
},
{
"path": [
"enableUserBadge"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"question": "Should a user be able to login with OpenStreetMap?",
"iftrue": "Enable the possiblity to login with OpenStreetMap",
"iffalse": "Do not enable to login with OpenStreetMap, have a read-only view of MapComplete.",
"ifunset": "Enable the possiblity to login with OpenStreetMap (default)"
},
"type": "boolean",
"description": "If set to false, disables logging in.\nThe userbadge will be hidden, all login-buttons will be hidden and editing will be disabled"
"description": "If not logged in, will not show the login buttons and hide all the editable elements.\nAs such, MapComplete will become read-only and a purely visualisation tool."
},
{
"path": [
"enableShareScreen"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"question": "Should the tab with options to share the current screen be enabled?",
"iftrue": "Enable the share screen",
"iffalse": "Do not enable the share screen",
"ifunset": "Enable the sharescreen (default)"
},
"type": "boolean",
"description": "If false, hides the tab 'share'-tab in the welcomeMessage"
"description": "On can get the iFrame embed code here"
},
{
"path": [
"enableMoreQuests"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"question": "Should the user be able to switch to different 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"
},
"type": "boolean",
"description": "Hides the tab with more themes in the welcomeMessage"
"description": "Typically enabled in iframes and/or on commisioned themes"
},
{
"path": [
"enableLayers"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"question": "Should the user be able to enable/disable layers and to filter the 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"
},
"type": "boolean",
"description": "If false, the layer selection/filter view will be hidden\nThe corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'"
"description": "The corresponding URL-parameter is 'fs-filters' instead of 'fs-layers'"
},
{
"path": [
"enableSearch"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"question": "Should the user be able to search for locations?",
"iftrue": "Allow to search",
"iffalse": "Do not allow to search; hide the search-bar",
"ifunset": "MapComplete default: allow to search"
},
"type": "boolean",
"description": "If set to false, hides the search bar"
"description": ""
},
{
"path": [
"enableAddNewPoints"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"question": "Should the user be able to add 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",
"ifunset": "MapComplete default: allow to create new points"
},
"type": "boolean",
"description": "If set to false, the ability to add new points or nodes will be disabled.\nEditing already existing features will still be possible"
"description": "Adding new points is only possible if the loaded layers have presets set.\nSome layers do not have presets. If the theme only has layers without presets, then adding new points will not be possible."
},
{
"path": [
"enableGeolocation"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"question": "Should the user be able to use their GPS to geolocate themselfes on the map?",
"iftrue": "Allow to use the GPS",
"iffalse": "Do not allow to use the GPS, hide the geolocation-buttons",
"ifunset": "MapComplete default: allow to use the GPS"
},
"type": "boolean",
"description": "If set to false, the 'geolocation'-button will be hidden."
"description": ""
},
{
"path": [
"enableBackgroundLayerSelection"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"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"
},
"type": "boolean",
"description": "Enable switching the backgroundlayer.\nIf false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well"
},
@ -34943,72 +35154,97 @@
"enableShowAllQuestions"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"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"
},
"type": "boolean",
"description": "If set to true, will show _all_ unanswered questions in a popup instead of just the next one"
"description": ""
},
{
"path": [
"enableDownload"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"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"
},
"type": "boolean",
"description": "If set to true, download button for the data will be shown (offers downloading as geojson and csv)"
"description": ""
},
{
"path": [
"enablePdfDownload"
],
"required": false,
"hints": {},
"hints": {
"group": "feature_switches",
"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"
},
"type": "boolean",
"description": "If set to true, exporting a pdf is enabled"
"description": ""
},
{
"path": [
"enableNoteImports"
],
"required": false,
"hints": {},
"hints": {
"group": "advanced",
"question": "Should the 'notes' from OpenStreetMap be loaded and parsed for import helper notes?",
"iftrue": "Load notes and show import notes",
"iffalse": "Do not load import notes",
"ifunset": "MapComplete default: do not load import notes for sideloaded themes but do load them for official themes"
},
"type": "boolean",
"description": "If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),\nthese notes will be shown if a relevant layer is present.\nDefault is true for official layers and false for unofficial (sideloaded) layers"
"description": "If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete),\nthese notes will be shown if a relevant layer is present."
},
{
"path": [
"overpassUrl"
],
"required": false,
"hints": {},
"type": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Set one or more overpass URLs to use for this theme.."
"hints": {
"group": "advanced",
"question": "What overpass-api instance should be used for this layout?",
"ifunset": "Use the default, builtin collection of overpass instances"
},
"type": "array",
"description": ""
},
{
"path": [
"overpassTimeout"
],
"required": false,
"hints": {},
"hints": {
"typehint": "pnat",
"group": "advanced",
"question": "After how much seconds should the overpass-query stop?",
"ifunset": "use the default amount of 30 seconds as timeout"
},
"type": "number",
"description": "Set a different timeout for overpass queries - in seconds. Default: 30s"
"description": "If a query takes too long, the overpass-server will abort.\nOnce can set the amount of time before overpass gives up here."
},
{
"path": [
"enableNodeDatabase"
],
"required": false,
"hints": {},
"hints": {
"group": "hidden"
},
"type": "boolean",
"description": "Enables tracking of all nodes when data is loaded.\nThis is useful for the 'ImportWay' and 'ConflateWay'-buttons who need this database.\nNote: this flag will be automatically set."
"description": "Enables tracking of all nodes when data is loaded.\nThis is useful for the 'ImportWay' and 'ConflateWay'-buttons who need this database.\nNote: this flag will be automatically set and can thus be ignored."
}
]