diff --git a/CHANGELOG.md b/CHANGELOG.md index 697d1d89..b8861ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## next release +* Enable export of geometries as geojson by using the attribute "allowGeometryExport" ([PR#137](https://github.com/mapbender/mapbender-digitizer/pull/137)) + + ## 2.0.3 * updateMultiple now returns a Promise; TableRenderer includes addOrRefreshRow for automatic feature handling post-AJAX call; unique namespace ID for unsaved features enables updates post-AJAX; oldGeometry set after successful updateMultiple call. ([PR#133](https://github.com/mapbender/mapbender-digitizer/pull/133)) * prevent Draw Donut On Non-Digitizer Features ([PR#120](https://github.com/mapbender/mapbender-digitizer/pull/120)) diff --git a/src/Mapbender/DataManagerBundle/Resources/styles/dataManager.element.scss b/src/Mapbender/DataManagerBundle/Resources/styles/dataManager.element.scss index 62cb2b92..82123a97 100644 --- a/src/Mapbender/DataManagerBundle/Resources/styles/dataManager.element.scss +++ b/src/Mapbender/DataManagerBundle/Resources/styles/dataManager.element.scss @@ -157,7 +157,7 @@ $buttonActiveTextColor: #fff !default; .ui-dialog-titlebar-close { &:before { content: "\f00d"; // =.fa-times - font-family: "FontAwesome", "Font Awesome 5 Free", "Font Awesome 5 Pro"; + font-family: "FontAwesome", "Font Awesome 6 Free", "Font Awesome 6 Pro", "Font Awesome 5 Free", "Font Awesome 5 Pro"; } .ui-button-icon { @@ -334,3 +334,37 @@ $buttonActiveTextColor: #fff !default; margin-right: 0.5em; } } + + +.data-manager-dialog, .mb-element-data-manager { + .sorting, .sorting_asc, .sorting_desc { + cursor: pointer; + } + + .sorting:before, .sorting_asc:before, .sorting_desc:after { + display: inline-block !important; + min-width: 1.4em; // Prevent width change on sorting state change + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); + line-height: 1; + transform: translateY(1px); + right: 1em !important; + } + + .sorting:before { + content: "\f0dc" !important; // = .fa-sort / .fa-unsorted + opacity: 1 !important; + } + + .sorting_asc:before { + content: "\f160" !important; // = .fa-sort-amount-asc + } + + .sorting_desc:after { + content: "\f161" !important; // = .fa-sort-amount-desc + } + + .sorting:after, .sorting_asc:after, .sorting_desc:before { + content: "" !important; + } +} diff --git a/src/Mapbender/DigitizerBundle/Resources/public/TableRenderer.js b/src/Mapbender/DigitizerBundle/Resources/public/TableRenderer.js index cb397950..4bd988fa 100644 --- a/src/Mapbender/DigitizerBundle/Resources/public/TableRenderer.js +++ b/src/Mapbender/DigitizerBundle/Resources/public/TableRenderer.js @@ -2,22 +2,32 @@ "use strict"; Mapbender.Digitizer = Mapbender.Digitizer || {}; + Mapbender.Digitizer.TableRenderer = function() { + // Call parent constructor Mapbender.DataManager.TableRenderer.apply(this, arguments); + + // Initialize an array to store checked row IDs + this.selectedFeatures = []; }; - Mapbender.Digitizer.TableRenderer.prototype = Object.create(Mapbender.DataManager.TableRenderer.prototype); - Object.assign(Mapbender.Digitizer.TableRenderer.prototype, { - constructor: Mapbender.Digitizer.TableRenderer - }); + + // Inherit from DataManager.TableRenderer + Mapbender.Digitizer.TableRenderer.prototype = + Object.create(Mapbender.DataManager.TableRenderer.prototype); Object.assign(Mapbender.Digitizer.TableRenderer.prototype, { + constructor: Mapbender.Digitizer.TableRenderer, + render: function(schema) { var table = Mapbender.DataManager.TableRenderer.prototype.render.call(this, schema); this.registerEvents(schema, $(table)); return table; }, + registerEvents: function(schema, $table) { var widget = this.owner; + + // Hover highlight $table.on('mouseenter mouseleave', 'tbody > tr', function(event) { var hover = event.handleObj.origType === 'mouseenter'; var feature = $(this).data().item; @@ -25,11 +35,11 @@ feature.set('hover', hover); } }); + + // Row click => zoom to feature $table.on('click', 'tbody > tr', function (e) { - // Do nothing if click hit an interaction button; return true to allow other handlers var $target = $(e.target); - var $parentsAndSelf = $target.parentsUntil(this).add($target); - if ($parentsAndSelf.filter('.button,.btn').length) { + if ($target.is('.row-checkbox')) { return true; } var feature = $(this).data().item; @@ -37,36 +47,64 @@ widget.zoomToFeature(schema, feature); } }); + + + // Existing buttons this.registerButtonEvents(schema, $table); }, + registerButtonEvents: function(schema, $table) { var self = this; - $table.on('click', 'tbody > tr .-fn-save', function(event) { - // Avoid calling row click handlers (may zoom to feature or open the edit dialog, depending on schema config) + + $table.on('click', '.-fn-check-for-export', function(e) { + e.stopPropagation(); + var $row = $(this).closest('tr'); + var feature = $row.data('item'); + if (!feature) { return; } + // Toggle selected state, maintain selectedFeatures array + var idx = self.selectedFeatures.indexOf(feature); + var selected = (idx >= 0); + if (selected) { + self.selectedFeatures.splice(idx, 1); + $('i', this).removeClass('fa-check-square').addClass('fa-square'); + } else { + self.selectedFeatures.push(feature); + $('i', this).removeClass('fa-square').addClass('fa-check-square'); + } + }); + + // Save + $table.on('click', '.-fn-save', function(event) { event.stopPropagation(); var data = $(this).closest('tr').data(); if (data.schema && data.item) { - self.owner._saveItem(data.schema, data.item).fail(function(){ - self.owner._afterFailedSave(data.schema,data.item); - }); + self.owner._saveItem(data.schema, data.item) + .fail(function(){ + self.owner._afterFailedSave(data.schema, data.item); + }); } }); - $table.on('click', 'tbody > tr .-fn-toggle-visibility', function(event) { - // Avoid calling row click handlers (may zoom to feature or open the edit dialog, depending on schema config) + + // Toggle visibility + $table.on('click', '.-fn-toggle-visibility', function(event) { event.stopPropagation(); var $tr = $(this).closest('tr'); var feature = $tr.data().item; feature.set('hidden', !feature.get('hidden')); self.updateButtonStates_($tr.get(0), feature); }); - $table.on('click', 'tbody > tr .-fn-edit-style', function(event) { + + // Edit style + $table.on('click', '.-fn-edit-style', function(event) { + event.stopPropagation(); var data = $(this).closest('tr').data(); if (data.schema && data.item) { self.owner.openStyleEditor(data.schema, data.item); } }); - $table.on('click', 'tbody > tr .-fn-copy', function(event) { - // Avoid calling row click handlers (may already try to zoom to feature, or open the edit dialog, depending on schema config) + + // Copy + $table.on('click', '.-fn-copy', function(event) { event.stopPropagation(); var data = $(this).closest('tr').data(); if (data.schema && data.item) { @@ -74,33 +112,29 @@ } }); }, + onRowCreation: function(tableSchema, tr, feature) { Mapbender.DataManager.TableRenderer.prototype.onRowCreation.apply(this, arguments); - // Place table row into feature data for quick access (synchronized highlighting etc) + // Link table row with feature feature.set('table-row', tr); // Inline save buttons start out disabled $('.-fn-save', tr).prop('disabled', !feature.get('dirty')); this.registerFeatureEvents(feature); }, + registerFeatureEvents: function(feature) { - // Avoid registering same event handlers on the same feature multiple times if (feature.get('table-events')) { return; } - // @todo: track across multiple tables - var self = this; - // Update interaction buttons when "hidden" and "dirty" values change feature.on(ol.ObjectEventType.PROPERTYCHANGE, function(event) { var feature = event.target; var tr = feature && feature.get('table-row'); if (tr) { switch (event.key) { default: - // do nothing break; case 'dirty': - // Page to feature on initial modification if (feature.get('dirty')) { self.showRow(tr); } @@ -113,7 +147,6 @@ break; } - if (event.key === 'dirty' && feature.get('dirty')) { self.showRow(tr); } @@ -122,34 +155,28 @@ }); feature.set('table-events', true); }, - /** - * @param {Object} feature - * @param {Boolean} show to automatically update pagination - */ + refreshRow: function(feature, show) { Mapbender.DataManager.TableRenderer.prototype.refreshRow.apply(this, arguments); - var tr = feature && feature.get('table-row'); if (tr) { this.updateButtonStates_(tr, feature); } }, + updateButtonStates_: function(tr, feature) { var hidden = !!feature.get('hidden'); - var tooltip; - if (hidden) { - tooltip = Mapbender.trans('mb.digitizer.feature.visibility.toggleon') - } else { - tooltip = Mapbender.trans('mb.digitizer.feature.visibility.toggleoff') - } + var tooltip = hidden + ? Mapbender.trans('mb.digitizer.feature.visibility.toggleon') + : Mapbender.trans('mb.digitizer.feature.visibility.toggleoff'); var $visibilityButton = $('.-fn-toggle-visibility', tr); - // Support both icon class ON button (legacy misuse) and icon markup INSIDE button transparently + // Handle icon class toggling var $visibilityIcon = $visibilityButton.children().add($visibilityButton).filter('.fa'); $visibilityIcon .toggleClass('fa-eye-slash', hidden) .toggleClass('fa-eye', !hidden) - .attr('title', tooltip) - ; + .attr('title', tooltip); + $('.-fn-save', tr).prop('disabled', !feature.get('dirty')); if (!!feature.get('dirty')) { diff --git a/src/Mapbender/DigitizerBundle/Resources/public/mapbender.element.digitizer.js b/src/Mapbender/DigitizerBundle/Resources/public/mapbender.element.digitizer.js index 711e01d9..6f7f06e8 100644 --- a/src/Mapbender/DigitizerBundle/Resources/public/mapbender.element.digitizer.js +++ b/src/Mapbender/DigitizerBundle/Resources/public/mapbender.element.digitizer.js @@ -307,6 +307,11 @@ } var $geometryToolGroup = $('.-js-drawing-tools', $toolset); $geometryToolGroup.empty().append(this.toolsetRenderer.renderGeometryToolButtons(schema)); + + if (!(schema.allowGeometryExport || subSchemas.some(subSchema => subSchema.allowGeometryExport))) { + console.warn("No export button for schema", schema); + $('.-js-export', $toolset).hide(); + } }, _openEditDialog: function(schema, feature) { // Make feature visible in table when editing was started @@ -422,7 +427,8 @@ schema.allowDigitize && schema.allowEdit && '-fn-save', schema.copy && schema.copy.enable && '-fn-copy', schema.allowCustomStyle && '-fn-edit-style', - schema.allowChangeVisibility && '-fn-toggle-visibility' + schema.allowChangeVisibility && '-fn-toggle-visibility', + schema.allowGeometryExport && '-fn-check-for-export' ]); return codes.filter(function(x) { return !!x; }); }, @@ -478,6 +484,7 @@ return this._super(schema).then(function(features) { self.queuedRefresh_[schema.schemaName] = false; self.renderer.replaceFeatures(schema, features); + self.tableRenderer.selectedFeatures = []; return features; }); }, @@ -796,6 +803,23 @@ adjustStyle: function(schema, feature) { }, + createExportData: function(geojson) { + + var geojsonstring = JSON.stringify(geojson); + this._downloadBlob(geojsonstring, 'application/json', 'export.geojson'); + }, + + _downloadBlob: function(data, mimeType, filename) { + let blob = (data instanceof Blob) ? data : new Blob([data], { type: mimeType }); + let url = URL.createObjectURL(blob); + let tempLink = document.createElement('a'); + tempLink.href = url; + tempLink.download = filename; + document.body.appendChild(tempLink); + tempLink.click(); + document.body.removeChild(tempLink); + }, + __formatting_dummy: null }); diff --git a/src/Mapbender/DigitizerBundle/Resources/public/toolset.js b/src/Mapbender/DigitizerBundle/Resources/public/toolset.js index df6cbac7..94661305 100644 --- a/src/Mapbender/DigitizerBundle/Resources/public/toolset.js +++ b/src/Mapbender/DigitizerBundle/Resources/public/toolset.js @@ -71,7 +71,7 @@ return (!seen[toolSpec.type]) && self.checkToolAccess_(subSchemas[s], toolSpec.type) && -1 !== validNames.indexOf(toolSpec.type) - ; + ; }); for (var t = 0; t < subSchemaTools.length; ++t) { var toolSpec = subSchemaTools[t]; @@ -102,10 +102,10 @@ .append($icon) .data({ schema: btnSchema - }) - ; + }); buttons.push($button); } + return buttons; }, checkToolAccess_: function(schema, toolName) { @@ -126,6 +126,45 @@ } }); }); + + // Add an event listener for exporting the currently selected features + widget.element.on('click', '.-fn-export-selected', function() { + // Get the selected features + var exportFeatures = widget.tableRenderer.selectedFeatures || []; + + // Check if there are any features to export + if (exportFeatures.length === 0) { + $.notify(Mapbender.trans('mb.digitizer.feature.export.notFound')); + return; + } + + // Create a new array of features that include geometry and properties + var strippedFeatures = exportFeatures.map(function(originalFeature) { + var geometryClone = originalFeature.getGeometry().clone(); + var properties = originalFeature.get("data"); + delete properties.geometry; // Remove the geometry property to avoid duplication + return new ol.Feature({ + geometry: geometryClone, + ...properties // Add the properties to the new feature + }); + }); + + var format = new ol.format.GeoJSON(); + var dataProjection = 'EPSG:4326'; + + // Write features as GeoJSON, specifying only geometry + var geojson = format.writeFeaturesObject(strippedFeatures, { + dataProjection: dataProjection, + featureProjection: widget.mbMap.getModel().olMap.getView().getProjection() + }); + + + widget.createExportData(geojson); + + + + }); + } }; })(jQuery); diff --git a/src/Mapbender/DigitizerBundle/Resources/translations/messages.de.yaml b/src/Mapbender/DigitizerBundle/Resources/translations/messages.de.yaml index e22f18b8..d0519e78 100644 --- a/src/Mapbender/DigitizerBundle/Resources/translations/messages.de.yaml +++ b/src/Mapbender/DigitizerBundle/Resources/translations/messages.de.yaml @@ -13,6 +13,9 @@ mb.digitizer: revert.geometry: Geometrieänderung zurücknehmen intersection.error: Ungültige Geometrie feature: + export: + check: 'Zum Exportieren auswählen' + notFound: 'Keine Objekte zum Exportieren ausgewählt' remove: title: 'Löschen' zoomTo: 'Heranzoomen' @@ -41,6 +44,7 @@ mb.digitizer: drawCircle: 'Kreis erstellen' drawEllipse: 'Ellipse erstellen' drawDonut: 'Enklave in Fläche zeichnen' + exportSelected: 'Ausgewählte Objekte exportieren' style: fillColor: Füllfarbe color: Color diff --git a/src/Mapbender/DigitizerBundle/Resources/translations/messages.en.yaml b/src/Mapbender/DigitizerBundle/Resources/translations/messages.en.yaml index e95cc3b3..91a00666 100644 --- a/src/Mapbender/DigitizerBundle/Resources/translations/messages.en.yaml +++ b/src/Mapbender/DigitizerBundle/Resources/translations/messages.en.yaml @@ -13,6 +13,9 @@ mb.digitizer: revert.geometry: Revert geometry changes intersection.error: Self-intersection in geometry not allowed feature: + export: + check: 'Select for export' + notFound: 'No objects selected for export' remove: title: 'Remove feature' zoomTo: 'Zoom to feature' diff --git a/src/Mapbender/DigitizerBundle/Resources/translations/messages.es.yaml b/src/Mapbender/DigitizerBundle/Resources/translations/messages.es.yaml index acb57db2..721c37b9 100644 --- a/src/Mapbender/DigitizerBundle/Resources/translations/messages.es.yaml +++ b/src/Mapbender/DigitizerBundle/Resources/translations/messages.es.yaml @@ -12,6 +12,9 @@ mb.digitizer: edit.geometry: Editar geometrías revert.geometry: Revertir los cambios en la geometría feature: + export: + check: 'Seleccionar para exportar' + notFound: 'No se han seleccionado objetos para exportar' remove: title: 'Eliminar' zoomTo: 'Zoom al objeto ' diff --git a/src/Mapbender/DigitizerBundle/Resources/translations/messages.it.yaml b/src/Mapbender/DigitizerBundle/Resources/translations/messages.it.yaml index d0baa37a..f2d99b10 100644 --- a/src/Mapbender/DigitizerBundle/Resources/translations/messages.it.yaml +++ b/src/Mapbender/DigitizerBundle/Resources/translations/messages.it.yaml @@ -12,6 +12,9 @@ mb.digitizer: edit.geometry: Modifica geometria revert.geometry: Annulla modifica geometria feature: + export: + check: 'Seleziona per esportare' + notFound: 'Nessun oggetto selezionato per l''esportazione' remove: title: 'Rimuovi' zoomTo: 'Ingrandisci' diff --git a/src/Mapbender/DigitizerBundle/Resources/translations/messages.pt.yaml b/src/Mapbender/DigitizerBundle/Resources/translations/messages.pt.yaml index 401c2ba5..bfe524f4 100644 --- a/src/Mapbender/DigitizerBundle/Resources/translations/messages.pt.yaml +++ b/src/Mapbender/DigitizerBundle/Resources/translations/messages.pt.yaml @@ -12,6 +12,9 @@ mb.digitizer: revert.geometry: Reverter alteração na geometria intersection.error: Geometria inválida feature: + export: + check: Selecionar para exportar + notFound: Nenhum objeto selecionado para exportar remove: title: Excluir zoomTo: Aproximar @@ -46,7 +49,7 @@ mb.digitizer: opacity: Opacidade pointRadius: Tamanho do ponto (pixels) strokeColor: Cor da linha - strokeWidth: Espessura da linha + strokeWidth: Espessura da linha lineCap: Extremidades da linha round: Arredondado square: Quadrado diff --git a/src/Mapbender/DigitizerBundle/Resources/translations/messages.ru.yaml b/src/Mapbender/DigitizerBundle/Resources/translations/messages.ru.yaml index b34c0927..8c6fc22d 100644 --- a/src/Mapbender/DigitizerBundle/Resources/translations/messages.ru.yaml +++ b/src/Mapbender/DigitizerBundle/Resources/translations/messages.ru.yaml @@ -6,14 +6,17 @@ mb.digitizer: delete: Удалить save: Сохранить feature: + export: + check: 'Выбрать для экспорта' + notFound: 'Объекты для экспорта не выбраны' remove: title: 'Удалить' - zoomTo: 'Zoom to' + zoomTo: 'Приблизить к объекту' edit: 'Редактировать' attributes: 'Атрибуты' save: title: 'Сохранить' - print: 'Print' + print: 'Печать' class: title: Сбор данных (Digitizer) description: Сбор и редактирование данных diff --git a/src/Mapbender/DigitizerBundle/Resources/views/Element/Digitizer.html.twig b/src/Mapbender/DigitizerBundle/Resources/views/Element/Digitizer.html.twig index 6b188e75..660abf71 100644 --- a/src/Mapbender/DigitizerBundle/Resources/views/Element/Digitizer.html.twig +++ b/src/Mapbender/DigitizerBundle/Resources/views/Element/Digitizer.html.twig @@ -1,5 +1,9 @@ {%- extends '@MapbenderDataManager/Element/DataManager.html.twig' -%} + {%- block table_buttons -%} + @@ -17,6 +21,7 @@ title="{{ 'mb.digitizer.feature.save.title' | trans }}"> {{- block('delete_button') -}} {%- endblock -%} + {%- block toolset -%}
@@ -24,6 +29,7 @@
{{- parent() -}} {%- endblock -%} + {%- block toolset_buttons -%} {{- parent() -}} {%- endblock -%} + {%- block toolset_groups -%} {{- parent() -}}
+
+ +
{%- endblock -%}