Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
}
99 changes: 63 additions & 36 deletions src/Mapbender/DigitizerBundle/Resources/public/TableRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,105 +2,139 @@
"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;
if (feature) {
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;
if (feature) {
widget.zoomToFeature(schema, feature);
}
});
this.registerButtonEvents(schema, $table);


// Existing buttons
this.registerButtonEvents(schema, $table);
},

registerButtonEvents: function(schema, $table) {
var self = this;

$table.on('click', 'tbody > tr .-fn-check-for-export', function(e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the specific tbody > tr and not just the .fn-check-for-export?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because I did not want to break the pattern used for the other click events

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I see. I'd vote for removing the tbody > tr for all of the click listeners, the more separation between markup and logic the more maintainable the code

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.fa', this).removeClass('fa-check-square-o').addClass('fa-square-o');
} else {
self.selectedFeatures.push(feature);
$('i.fa', this).removeClass('fa-square-o').addClass('fa-check-square-o');
}
});

// Save
$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)
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);
});
}
});

// Toggle visibility
$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)
event.stopPropagation();
var $tr = $(this).closest('tr');
var feature = $tr.data().item;
feature.set('hidden', !feature.get('hidden'));
self.updateButtonStates_($tr.get(0), feature);
});

// Edit style
$table.on('click', 'tbody > tr .-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);
}
});

// Copy
$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)
event.stopPropagation();
var data = $(this).closest('tr').data();
if (data.schema && data.item) {
self.owner.cloneFeature(data.schema, data.item);
}
});
},

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);
}
Expand All @@ -113,7 +147,6 @@
break;
}


if (event.key === 'dirty' && feature.get('dirty')) {
self.showRow(tr);
}
Expand All @@ -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')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,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; });
},
Expand Down Expand Up @@ -478,6 +479,7 @@
return this._super(schema).then(function(features) {
self.queuedRefresh_[schema.schemaName] = false;
self.renderer.replaceFeatures(schema, features);
self.tableRenderer.selectedFeatures = [];
return features;
});
},
Expand Down Expand Up @@ -796,6 +798,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
});

Expand Down
57 changes: 54 additions & 3 deletions src/Mapbender/DigitizerBundle/Resources/public/toolset.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -102,10 +102,22 @@
.append($icon)
.data({
schema: btnSchema
})
;
});
buttons.push($button);
}

if (schema.allowGeometryExport || this.owner.expandCombination(schema).some(subSchema => subSchema.allowGeometryExport)) {
var $exportBtnIcon = $('<span>').addClass('fa fa-download');
var $exportBtn = $('<button>')
.attr({
type: 'button',
title: Mapbender.trans('mb.digitizer.toolset.exportSelected')
})
.addClass('btn btn-default -fn-export-selected')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button's style does not fit into the style of the other buttons:

grafik

Also, it does not really fit into the geometry manipulation group. I'd either put it into the first button group or make it its own group.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

.append($exportBtnIcon);
buttons.push($exportBtn);
}

return buttons;
},
checkToolAccess_: function(schema, toolName) {
Expand All @@ -126,6 +138,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);
Loading