Finish the export functionality: move logic around a bit, add license information for reusers, wire the functionality as feature switch

This commit is contained in:
pietervdvn 2021-07-16 01:42:09 +02:00
parent 3001a563d2
commit abd7db100d
10 changed files with 98 additions and 65 deletions

View file

@ -42,6 +42,7 @@ export default class LayoutConfig {
public readonly enableGeolocation: boolean; public readonly enableGeolocation: boolean;
public readonly enableBackgroundLayerSelection: boolean; public readonly enableBackgroundLayerSelection: boolean;
public readonly enableShowAllQuestions: boolean; public readonly enableShowAllQuestions: boolean;
public readonly enableExportButton: boolean;
public readonly customCss?: string; public readonly customCss?: string;
/* /*
How long is the cache valid, in seconds? How long is the cache valid, in seconds?
@ -152,6 +153,7 @@ export default class LayoutConfig {
this.enableAddNewPoints = json.enableAddNewPoints ?? true; this.enableAddNewPoints = json.enableAddNewPoints ?? true;
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true; this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
this.enableExportButton = json.enableExportButton ?? false;
this.customCss = json.customCss; this.customCss = json.customCss;
this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)

View file

@ -15,6 +15,7 @@ import UnitConfigJson from "./UnitConfigJson";
* General remark: a type (string | any) indicates either a fixed or a translatable string. * General remark: a type (string | any) indicates either a fixed or a translatable string.
*/ */
export interface LayoutConfigJson { export interface LayoutConfigJson {
/** /**
* The id of this layout. * The id of this layout.
* *
@ -335,4 +336,5 @@ export interface LayoutConfigJson {
enableGeolocation?: boolean; enableGeolocation?: boolean;
enableBackgroundLayerSelection?: boolean; enableBackgroundLayerSelection?: boolean;
enableShowAllQuestions?: boolean; enableShowAllQuestions?: boolean;
enableExportButton?: boolean;
} }

View file

@ -1,9 +1,45 @@
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
export default interface FeatureSource { export default interface FeatureSource {
features: UIEventSource<{feature: any, freshness: Date}[]>; features: UIEventSource<{ feature: any, freshness: Date }[]>;
/** /**
* Mainly used for debuging * Mainly used for debuging
*/ */
name: string; name: string;
}
export class FeatureSourceUtils {
/**
* Exports given featurePipeline as a geojson FeatureLists (downloads as a json)
* @param featurePipeline The FeaturePipeline you want to export
* @param options The options object
* @param options.metadata True if you want to include the MapComplete metadata, false otherwise
*/
public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) {
let defaults = {
metadata: false,
}
options = Utils.setDefaults(options, defaults);
// Select all features, ignore the freshness and other data
let featureList: any[] = featurePipeline.features.data.map((feature) => feature.feature);
if (!options.metadata) {
for (let i = 0; i < featureList.length; i++) {
let feature = featureList[i];
for (let property in feature.properties) {
if (property[0] == "_") {
delete featureList[i]["properties"][property];
}
}
}
}
return {type: "FeatureCollection", features: featureList}
}
} }

View file

@ -1,39 +0,0 @@
import FeaturePipeline from "./FeaturePipeline";
import {Utils} from "../../Utils";
/**
* Exports given featurePipeline as a geojson FeatureLists (downloads as a json)
* @param featurePipeline The FeaturePipeline you want to export
* @param options The options object
* @param options.metadata True if you want to include the MapComplete metadata, false otherwise
*/
export function exportAsGeoJson(featurePipeline: FeaturePipeline, options: { metadata?: boolean} = {}) {
let defaults = {
metadata: false,
}
options = Utils.setDefaults(options, defaults);
// Select all features, ignore the freshness and other data
let featureList: JSON[] = featurePipeline ? featurePipeline.features.data.map((feature) => feature.feature) : ["I'm empty"];
/**
* Removes the metadata of MapComplete (all properties starting with an underscore)
* @param featureList JsonList containing features, output object
*/
function removeMetaData(featureList: JSON[]) {
for (let i=0; i < featureList.length; i++) {
let feature = featureList[i];
for (let property in feature.properties) {
if (property[0] == "_") {
delete featureList[i]["properties"][property];
}
}
}
}
if (!options.metadata) removeMetaData(featureList);
let geojson = {type: "FeatureCollection", features: featureList}
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "Geodata.json");
}

View file

@ -96,6 +96,10 @@ export default class State {
public readonly featureSwitchIsDebugging: UIEventSource<boolean>; public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
public readonly featureSwitchApiURL: UIEventSource<string>; public readonly featureSwitchApiURL: UIEventSource<string>;
public readonly featureSwitchEnableExport: UIEventSource<boolean>;
public readonly featurePipeline: FeaturePipeline; public readonly featurePipeline: FeaturePipeline;
@ -127,7 +131,7 @@ export default class State {
public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map<number>( public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map<number>(
str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n
); );
constructor(layoutToUse: LayoutConfig) { constructor(layoutToUse: LayoutConfig) {
const self = this; const self = this;
@ -201,6 +205,8 @@ export default class State {
"Disables/Enables the geolocation button"); "Disables/Enables the geolocation button");
this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false,
"Always show all questions"); "Always show all questions");
this.featureSwitchEnableExport = featSw("fs-export",(layoutToUse) => layoutToUse?.enableExportButton ?? false,
"If set, enables the 'download'-button to download everything as geojson")
this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false", this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false",
"If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org") "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org")
@ -212,7 +218,7 @@ export default class State {
this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend","osm", this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend","osm",
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'") "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'")
} }
{ {
// Some other feature switches // Some other feature switches

2
Svg.ts
View file

@ -94,7 +94,7 @@ export default class Svg {
public static crosshair_empty_svg() { return new Img(Svg.crosshair_empty, true);} public static crosshair_empty_svg() { return new Img(Svg.crosshair_empty, true);}
public static crosshair_empty_ui() { return new FixedUiElement(Svg.crosshair_empty_img);} public static crosshair_empty_ui() { return new FixedUiElement(Svg.crosshair_empty_img);}
public static crosshair_locked = " <!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" width=\"100\" height=\"100\" viewBox=\"0 0 26.458333 26.458334\" version=\"1.1\" id=\"svg8\" inkscape:version=\"0.92.5 (2060ec1f9f, 2020-04-08)\" sodipodi:docname=\"crosshair-locked.svg\"> <defs id=\"defs2\" /> <sodipodi:namedview id=\"base\" pagecolor=\"#ffffff\" bordercolor=\"#666666\" borderopacity=\"1.0\" inkscape:pageopacity=\"0.0\" inkscape:pageshadow=\"2\" inkscape:zoom=\"2.8284271\" inkscape:cx=\"67.47399\" inkscape:cy=\"29.788021\" inkscape:document-units=\"px\" inkscape:current-layer=\"layer1\" showgrid=\"false\" units=\"px\" showguides=\"true\" inkscape:guide-bbox=\"true\" inkscape:window-width=\"1920\" inkscape:window-height=\"999\" inkscape:window-x=\"0\" inkscape:window-y=\"0\" inkscape:window-maximized=\"1\" inkscape:snap-global=\"false\"> <sodipodi:guide position=\"13.229167,23.859748\" orientation=\"1,0\" id=\"guide815\" inkscape:locked=\"false\" /> <sodipodi:guide position=\"14.944824,13.229167\" orientation=\"0,1\" id=\"guide817\" inkscape:locked=\"false\" /> </sodipodi:namedview> <metadata id=\"metadata5\"> <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> <dc:title /> </cc:Work> </rdf:RDF> </metadata> <g inkscape:label=\"Layer 1\" inkscape:groupmode=\"layer\" id=\"layer1\" transform=\"translate(0,-270.54165)\"> <circle style=\"fill: none !important;fill-opacity:1;stroke:#5555ec;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529\" id=\"path815\" cx=\"13.16302\" cy=\"283.77081\" r=\"8.8715391\" /> <path style=\"fill: none !important;stroke:#5555ec;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" d=\"M 3.2841366,283.77082 H 1.0418969\" id=\"path817\" inkscape:connector-curvature=\"0\" /> <path style=\"fill: none !important;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" d=\"M 25.405696,283.77082 H 23.286471\" id=\"path817-3\" inkscape:connector-curvature=\"0\" /> <path style=\"fill: none !important;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" d=\"m 13.229167,295.9489 v -2.11763\" id=\"path817-3-6\" inkscape:connector-curvature=\"0\" /> <path style=\"fill: none !important;stroke:#5555ec;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" d=\"m 13.229167,275.05759 v -3.44507\" id=\"path817-3-6-7\" inkscape:connector-curvature=\"0\" /> <g id=\"g824\" transform=\"matrix(0.63953151,0,0,0.64343684,6.732623,276.71502)\" style=\"fill:#ffcc33\"> <path id=\"path822\" d=\"M 16.07,8 H 15 V 5 C 15,5 15,0 10,0 5,0 5,5 5,5 V 8 H 3.93 A 1.93,1.93 0 0 0 2,9.93 v 8.15 A 1.93,1.93 0 0 0 3.93,20 H 16.07 A 1.93,1.93 0 0 0 18,18.07 V 9.93 A 1.93,1.93 0 0 0 16.07,8 Z M 10,16 a 2,2 0 1 1 2,-2 2,2 0 0 1 -2,2 z M 13,8 H 7 V 5.5 C 7,4 7,2 10,2 c 3,0 3,2 3,3.5 z\" inkscape:connector-curvature=\"0\" /> </g> </g> </svg> " public static crosshair_locked = " <!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" width=\"100\" height=\"100\" viewBox=\"0 0 26.458333 26.458334\" version=\"1.1\" id=\"svg8\" inkscape:version=\"0.92.5 (2060ec1f9f, 2020-04-08)\" sodipodi:docname=\"crosshair-locked.svg\"> <defs id=\"defs2\" /> <sodipodi:namedview id=\"base\" pagecolor=\"#ffffff\" bordercolor=\"#666666\" borderopacity=\"1.0\" inkscape:pageopacity=\"0.0\" inkscape:pageshadow=\"2\" inkscape:zoom=\"5.6568542\" inkscape:cx=\"27.044982\" inkscape:cy=\"77.667126\" inkscape:document-units=\"px\" inkscape:current-layer=\"layer1\" showgrid=\"false\" units=\"px\" showguides=\"true\" inkscape:guide-bbox=\"true\" inkscape:window-width=\"1920\" inkscape:window-height=\"999\" inkscape:window-x=\"0\" inkscape:window-y=\"0\" inkscape:window-maximized=\"1\" inkscape:snap-global=\"false\"> <sodipodi:guide position=\"13.229167,23.859748\" orientation=\"1,0\" id=\"guide815\" inkscape:locked=\"false\" /> <sodipodi:guide position=\"14.944824,13.229167\" orientation=\"0,1\" id=\"guide817\" inkscape:locked=\"false\" /> </sodipodi:namedview> <metadata id=\"metadata5\"> <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> <dc:title /> </cc:Work> </rdf:RDF> </metadata> <g inkscape:label=\"Layer 1\" inkscape:groupmode=\"layer\" id=\"layer1\" transform=\"translate(0,-270.54165)\"> <g id=\"g827\"> <circle r=\"8.8715391\" cy=\"283.77081\" cx=\"13.16302\" id=\"path815\" style=\"fill: none !important;fill-opacity:1;stroke:#5555ec;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529\" /> <path inkscape:connector-curvature=\"0\" id=\"path817\" d=\"M 3.2841366,283.77082 H 1.0418969\" style=\"fill: none !important;stroke:#5555ec;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" /> <path inkscape:connector-curvature=\"0\" id=\"path817-3\" d=\"M 25.405696,283.77082 H 23.286471\" style=\"fill: none !important;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" /> <path inkscape:connector-curvature=\"0\" id=\"path817-3-6\" d=\"m 13.229167,295.9489 v -2.11763\" style=\"fill: none !important;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" /> <path inkscape:connector-curvature=\"0\" id=\"path817-3-6-7\" d=\"m 13.229167,275.05759 v -3.44507\" style=\"fill: none !important;stroke:#5555ec;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529\" /> </g> <path style=\"fill:#5555ec;fill-opacity:0.98823529;stroke-width:0.6151033\" inkscape:connector-curvature=\"0\" d=\"m 16.850267,281.91543 h -0.65616 v -1.85094 c 0,0 0,-3.08489 -3.066169,-3.08489 -3.066169,0 -3.066169,3.08489 -3.066169,3.08489 v 1.85094 H 9.4056091 a 1.1835412,1.1907685 0 0 0 -1.1835412,1.19077 v 5.02838 a 1.1835412,1.1907685 0 0 0 1.1835412,1.1846 h 7.4446579 a 1.1835412,1.1907685 0 0 0 1.183541,-1.19078 v -5.0222 a 1.1835412,1.1907685 0 0 0 -1.183541,-1.19077 z m -3.722329,4.93583 a 1.2264675,1.233957 0 1 1 1.226468,-1.23395 1.2264675,1.233957 0 0 1 -1.226468,1.23395 z m 1.839702,-4.93583 h -3.679403 v -1.54245 c 0,-0.92546 0,-2.15942 1.839701,-2.15942 1.839702,0 1.839702,1.23396 1.839702,2.15942 z\" id=\"path822\" /> </g> </svg> "
public static crosshair_locked_img = Img.AsImageElement(Svg.crosshair_locked) public static crosshair_locked_img = Img.AsImageElement(Svg.crosshair_locked)
public static crosshair_locked_svg() { return new Img(Svg.crosshair_locked, true);} public static crosshair_locked_svg() { return new Img(Svg.crosshair_locked, true);}
public static crosshair_locked_ui() { return new FixedUiElement(Svg.crosshair_locked_img);} public static crosshair_locked_ui() { return new FixedUiElement(Svg.crosshair_locked_img);}

View file

@ -0,0 +1,21 @@
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import State from "../../State";
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
import {Utils} from "../../Utils";
import Combine from "../Base/Combine";
export class ExportDataButton extends Combine {
constructor() {
const t = Translations.t.general.download
const button = new SubtleButton(Svg.floppy_ui(), t.downloadGeojson.Clone().SetClass("font-bold"))
.onClick(() => {
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline)
const name = State.state.layoutToUse.data.id;
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), `MapComplete_${name}_export_${new Date().toISOString().substr(0,19)}.geojson`);
})
super([button, t.licenseInfo.Clone().SetClass("link-underline")])
}
}

View file

@ -2,11 +2,12 @@ import State from "../../State";
import BackgroundSelector from "./BackgroundSelector"; import BackgroundSelector from "./BackgroundSelector";
import LayerSelection from "./LayerSelection"; import LayerSelection from "./LayerSelection";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle";
import {ExportDataButton} from "./ExportDataButton";
export default class LayerControlPanel extends ScrollableFullScreen { export default class LayerControlPanel extends ScrollableFullScreen {
@ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen {
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown); super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
} }
private static GenTitle():BaseUIElement { private static GenTitle(): BaseUIElement {
return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2") return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2")
} }
private static GeneratePanel() : BaseUIElement { private static GeneratePanel(): BaseUIElement {
let layerControlPanel: BaseUIElement = new FixedUiElement(""); const elements: BaseUIElement[] = []
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
layerControlPanel = new BackgroundSelector(); const backgroundSelector = new BackgroundSelector();
layerControlPanel.SetStyle("margin:1em"); backgroundSelector.SetStyle("margin:1em");
layerControlPanel.onClick(() => { backgroundSelector.onClick(() => {
}); });
elements.push(backgroundSelector)
} }
if (State.state.filteredLayers.data.length > 1) { elements.push(new Toggle(
const layerSelection = new LayerSelection(State.state.filteredLayers); new LayerSelection(State.state.filteredLayers),
layerSelection.onClick(() => { undefined,
}); State.state.filteredLayers.map(layers => layers.length > 1)
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]); ))
}
return layerControlPanel; elements.push(new Toggle(
new ExportDataButton(),
undefined,
State.state.featureSwitchEnableExport
))
return new Combine(elements).SetClass("flex flex-col")
} }
} }

View file

@ -7,8 +7,6 @@ import Translations from "../i18n/Translations";
import LayerConfig from "../../Customizations/JSON/LayerConfig"; import LayerConfig from "../../Customizations/JSON/LayerConfig";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import {SubtleButton} from "../Base/SubtleButton";
import {exportAsGeoJson} from "../../Logic/FeatureSource/GeoJsonExport";
/** /**
* Shows the panel with all layers and a toggle for each of them * Shows the panel with all layers and a toggle for each of them
@ -76,10 +74,6 @@ export default class LayerSelection extends Combine {
); );
} }
const downloadButton = new SubtleButton("./assets/svg/floppy.svg", Translations.t.general.layerSelection.downloadGeojson.Clone())
downloadButton.onClick(() => exportAsGeoJson(State.state.featurePipeline))
checkboxes.push(downloadButton)
super(checkboxes) super(checkboxes)
this.SetStyle("display:flex;flex-direction:column;") this.SetStyle("display:flex;flex-direction:column;")

View file

@ -147,8 +147,11 @@
"loginOnlyNeededToEdit": "if you want to edit the map", "loginOnlyNeededToEdit": "if you want to edit the map",
"layerSelection": { "layerSelection": {
"zoomInToSeeThisLayer": "Zoom in to see this layer", "zoomInToSeeThisLayer": "Zoom in to see this layer",
"title": "Select layers", "title": "Select layers"
"downloadGeojson": "Download layer features as geojson" },
"download": {
"downloadGeojson": "Download visible data as geojson",
"licenseInfo": "<h3>Copyright notice</h3>The provided is available under ODbL. Reusing this data is free for any purpose, but <ul><li>the attribution <b>© OpenStreetMap contributors</b></li><li>Any change to this data must be republished under the same license</li></ul>. Please see the full <a href='https://www.openstreetmap.org/copyright' target='_blank'>copyright notice</a> for details"
}, },
"weekdays": { "weekdays": {
"abbreviations": { "abbreviations": {