diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index 12b9d5f766..e76c68ac88 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -42,6 +42,7 @@ export default class LayoutConfig { public readonly enableGeolocation: boolean; public readonly enableBackgroundLayerSelection: boolean; public readonly enableShowAllQuestions: boolean; + public readonly enableExportButton: boolean; public readonly customCss?: string; /* How long is the cache valid, in seconds? @@ -152,6 +153,7 @@ export default class LayoutConfig { this.enableAddNewPoints = json.enableAddNewPoints ?? true; this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true; this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; + this.enableExportButton = json.enableExportButton ?? false; this.customCss = json.customCss; this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts index 374de70e07..d36a8463d6 100644 --- a/Customizations/JSON/LayoutConfigJson.ts +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -15,6 +15,7 @@ import UnitConfigJson from "./UnitConfigJson"; * General remark: a type (string | any) indicates either a fixed or a translatable string. */ export interface LayoutConfigJson { + /** * The id of this layout. * @@ -335,4 +336,5 @@ export interface LayoutConfigJson { enableGeolocation?: boolean; enableBackgroundLayerSelection?: boolean; enableShowAllQuestions?: boolean; + enableExportButton?: boolean; } diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index ba568271ea..171db39f6a 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -1,9 +1,45 @@ import {UIEventSource} from "../UIEventSource"; +import {Utils} from "../../Utils"; export default interface FeatureSource { - features: UIEventSource<{feature: any, freshness: Date}[]>; + features: UIEventSource<{ feature: any, freshness: Date }[]>; /** * Mainly used for debuging */ 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} + + + } + + } \ No newline at end of file diff --git a/Logic/FeatureSource/GeoJsonExport.ts b/Logic/FeatureSource/GeoJsonExport.ts deleted file mode 100644 index 541e103730..0000000000 --- a/Logic/FeatureSource/GeoJsonExport.ts +++ /dev/null @@ -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"); -} diff --git a/State.ts b/State.ts index 7793485aa3..90289bcab4 100644 --- a/State.ts +++ b/State.ts @@ -96,6 +96,10 @@ export default class State { public readonly featureSwitchIsDebugging: UIEventSource; public readonly featureSwitchShowAllQuestions: UIEventSource; public readonly featureSwitchApiURL: UIEventSource; + public readonly featureSwitchEnableExport: UIEventSource; + + + 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( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n ); - + constructor(layoutToUse: LayoutConfig) { const self = this; @@ -201,6 +205,8 @@ export default class State { "Disables/Enables the geolocation button"); this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, "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", "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", "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'") - + } { // Some other feature switches diff --git a/Svg.ts b/Svg.ts index 0266e43e8e..5684fc4b56 100644 --- a/Svg.ts +++ b/Svg.ts @@ -94,7 +94,7 @@ export default class Svg { 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_locked = " image/svg+xml " + public static crosshair_locked = " image/svg+xml " 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_ui() { return new FixedUiElement(Svg.crosshair_locked_img);} diff --git a/UI/BigComponents/ExportDataButton.ts b/UI/BigComponents/ExportDataButton.ts new file mode 100644 index 0000000000..9a161de9fd --- /dev/null +++ b/UI/BigComponents/ExportDataButton.ts @@ -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")]) + } +} \ No newline at end of file diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index 42a3eda125..c8837fbccc 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -2,11 +2,12 @@ import State from "../../State"; import BackgroundSelector from "./BackgroundSelector"; import LayerSelection from "./LayerSelection"; import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import Translations from "../i18n/Translations"; import {UIEventSource} from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; +import Toggle from "../Input/Toggle"; +import {ExportDataButton} from "./ExportDataButton"; export default class LayerControlPanel extends ScrollableFullScreen { @@ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen { 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") } - private static GeneratePanel() : BaseUIElement { - let layerControlPanel: BaseUIElement = new FixedUiElement(""); + private static GeneratePanel(): BaseUIElement { + const elements: BaseUIElement[] = [] + if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { - layerControlPanel = new BackgroundSelector(); - layerControlPanel.SetStyle("margin:1em"); - layerControlPanel.onClick(() => { + const backgroundSelector = new BackgroundSelector(); + backgroundSelector.SetStyle("margin:1em"); + backgroundSelector.onClick(() => { }); + elements.push(backgroundSelector) } - if (State.state.filteredLayers.data.length > 1) { - const layerSelection = new LayerSelection(State.state.filteredLayers); - layerSelection.onClick(() => { - }); - layerControlPanel = new Combine([layerSelection, "
", layerControlPanel]); - } + elements.push(new Toggle( + new LayerSelection(State.state.filteredLayers), + undefined, + State.state.filteredLayers.map(layers => layers.length > 1) + )) - return layerControlPanel; + elements.push(new Toggle( + new ExportDataButton(), + undefined, + State.state.featureSwitchEnableExport + )) + + return new Combine(elements).SetClass("flex flex-col") } } \ No newline at end of file diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index c48b361633..3c7f108e8c 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -7,8 +7,6 @@ import Translations from "../i18n/Translations"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import BaseUIElement from "../BaseUIElement"; 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 @@ -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) this.SetStyle("display:flex;flex-direction:column;") diff --git a/langs/en.json b/langs/en.json index b604850f8f..aac35335c1 100644 --- a/langs/en.json +++ b/langs/en.json @@ -147,8 +147,11 @@ "loginOnlyNeededToEdit": "if you want to edit the map", "layerSelection": { "zoomInToSeeThisLayer": "Zoom in to see this layer", - "title": "Select layers", - "downloadGeojson": "Download layer features as geojson" + "title": "Select layers" + }, + "download": { + "downloadGeojson": "Download visible data as geojson", + "licenseInfo": "

Copyright notice

The provided is available under ODbL. Reusing this data is free for any purpose, but
  • the attribution © OpenStreetMap contributors
  • Any change to this data must be republished under the same license
. Please see the full copyright notice for details" }, "weekdays": { "abbreviations": {