diff --git a/web/package.json b/web/package.json
index a551af1..1025e28 100644
--- a/web/package.json
+++ b/web/package.json
@@ -74,6 +74,7 @@
"jquery": "3.3.1",
"jquery-contextmenu": "^2.6.4",
"jquery-ui": "^1.12.1",
+ "leaflet": "^1.3.3",
"moment": "^2.20.1",
"mousetrap": "^1.6.1",
"prop-types": "^15.5.10",
@@ -91,7 +92,8 @@
"underscore": "^1.8.3",
"underscore.string": "^3.3.4",
"watchify": "~3.9.0",
- "webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker"
+ "webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker",
+ "wkx": "^0.4.5"
},
"scripts": {
"linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js --ext .jsx .",
diff --git a/web/pgadmin/static/bundle/slickgrid.js b/web/pgadmin/static/bundle/slickgrid.js
index befb67e..09a5373 100644
--- a/web/pgadmin/static/bundle/slickgrid.js
+++ b/web/pgadmin/static/bundle/slickgrid.js
@@ -8,5 +8,7 @@ import 'slickgrid/slick.formatters';
import 'slickgrid/plugins/slick.autotooltips';
import 'slickgrid/plugins/slick.cellrangedecorator';
import 'slickgrid/plugins/slick.cellrangeselector';
+import 'sources/slickgrid/custom_header_buttons';
+
+export default window.Slick;
-export default window.Slick;
\ No newline at end of file
diff --git a/web/pgadmin/static/css/style.css b/web/pgadmin/static/css/style.css
index ed88eab..d9d39bf 100644
--- a/web/pgadmin/static/css/style.css
+++ b/web/pgadmin/static/css/style.css
@@ -14,6 +14,7 @@
@import '~webcabin-docker/Build/wcDocker.css';
@import '~acitree/css/aciTree.css';
@import '~spectrum-colorpicker/spectrum.css';
+@import '~leaflet/dist/leaflet.css';
@import '~codemirror/lib/codemirror.css';
@import '~codemirror/addon/dialog/dialog.css';
diff --git a/web/pgadmin/static/js/slickgrid/custom_header_buttons.js b/web/pgadmin/static/js/slickgrid/custom_header_buttons.js
new file mode 100644
index 0000000..90bef69
--- /dev/null
+++ b/web/pgadmin/static/js/slickgrid/custom_header_buttons.js
@@ -0,0 +1,180 @@
+(function ($) {
+ // register namespace
+ $.extend(true, window, {
+ 'Slick': {
+ 'Plugins': {
+ 'HeaderButtons': HeaderButtons,
+ },
+ },
+ });
+
+
+ /***
+ * custom header button modified from slick.headerbuttons.js
+ *
+ * USAGE:
+ *
+ * Add the plugin .js & .css files and register it with the grid.
+ *
+ * To specify a custom button in a column header, extend the column definition like so:
+ *
+ * var columns = [
+ * {
+ * id: 'myColumn',
+ * name: 'My column',
+ *
+ * // This is the relevant part
+ * header: {
+ * buttons: [
+ * {
+ * // button options
+ * },
+ * {
+ * // button options
+ * }
+ * ]
+ * }
+ * }
+ * ];
+ *
+ * Available button options:
+ * cssClass: CSS class to add to the button.
+ * image: Relative button image path.
+ * tooltip: Button tooltip.
+ * showOnHover: Only show the button on hover.
+ * handler: Button click handler.
+ * command: A command identifier to be passed to the onCommand event handlers.
+ *
+ * The plugin exposes the following events:
+ * onCommand: Fired on button click for buttons with 'command' specified.
+ * Event args:
+ * grid: Reference to the grid.
+ * column: Column definition.
+ * command: Button command identified.
+ * button: Button options. Note that you can change the button options in your
+ * event handler, and the column header will be automatically updated to
+ * reflect them. This is useful if you want to implement something like a
+ * toggle button.
+ *
+ *
+ * @param options {Object} Options:
+ * buttonCssClass: a CSS class to use for buttons (default 'slick-header-button')
+ * @class Slick.Plugins.HeaderButtons
+ * @constructor
+ */
+ function HeaderButtons(options) {
+ var _grid;
+ var _self = this;
+ var _handler = new window.Slick.EventHandler();
+ var _defaults = {
+ buttonCssClass: 'slick-header-button',
+ };
+
+
+ function init(grid) {
+ options = $.extend(true, {}, _defaults, options);
+ _grid = grid;
+ _handler
+ .subscribe(_grid.onHeaderCellRendered, handleHeaderCellRendered)
+ .subscribe(_grid.onBeforeHeaderCellDestroy, handleBeforeHeaderCellDestroy);
+
+ // Force the grid to re-render the header now that the events are hooked up.
+ _grid.setColumns(_grid.getColumns());
+ }
+
+
+ function destroy() {
+ _handler.unsubscribeAll();
+ }
+
+
+ function handleHeaderCellRendered(e, args) {
+ var column = args.column;
+
+ if (column.header && column.header.buttons) {
+ // Append buttons in reverse order since they are floated to the right.
+ var i = column.header.buttons.length;
+ while (i--) {
+ var button = column.header.buttons[i];
+ var btn = $('
')
+ .addClass(options.buttonCssClass)
+ .data('column', column)
+ .data('button', button);
+
+ if (button.content){
+ btn.append(button.content);
+ }
+
+ if (button.showOnHover) {
+ btn.addClass('slick-header-button-hidden');
+ }
+
+ if (button.image) {
+ btn.css('backgroundImage', 'url(' + button.image + ')');
+ }
+
+ if (button.cssClass) {
+ btn.addClass(button.cssClass);
+ }
+
+ if (button.tooltip) {
+ btn.attr('title', button.tooltip);
+ }
+
+ if (button.command) {
+ btn.data('command', button.command);
+ }
+
+ if (button.handler) {
+ btn.bind('click', button.handler);
+ }
+
+ btn
+ .bind('click', handleButtonClick)
+ .prependTo(args.node);
+ }
+ }
+ }
+
+
+ function handleBeforeHeaderCellDestroy(e, args) {
+ var column = args.column;
+
+ if (column.header && column.header.buttons) {
+ // Removing buttons via jQuery will also clean up any event handlers and data.
+ // NOTE: If you attach event handlers directly or using a different framework,
+ // you must also clean them up here to avoid memory leaks.
+ $(args.node).find('.' + options.buttonCssClass).remove();
+ }
+ }
+
+
+ function handleButtonClick(e) {
+ var command = $(this).data('command');
+ var columnDef = $(this).data('column');
+ var button = $(this).data('button');
+
+ if (command != null) {
+ _self.onCommand.notify({
+ 'grid': _grid,
+ 'column': columnDef,
+ 'command': command,
+ 'button': button,
+ }, e, _self);
+
+ // Update the header in case the user updated the button definition in the handler.
+ _grid.updateColumnHeader(columnDef.id);
+ }
+
+ // Stop propagation so that it doesn't register as a header click event.
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ $.extend(this, {
+ 'init': init,
+ 'destroy': destroy,
+ 'onCommand': new window.Slick.Event(),
+ });
+ }
+})(window.jQuery);
diff --git a/web/pgadmin/static/js/slickgrid/formatters.js b/web/pgadmin/static/js/slickgrid/formatters.js
index adfcf79..9da04dc 100644
--- a/web/pgadmin/static/js/slickgrid/formatters.js
+++ b/web/pgadmin/static/js/slickgrid/formatters.js
@@ -4,6 +4,8 @@
* @module Formatters
* @namespace Slick
*/
+import {Geometry} from 'wkx';
+import {Buffer} from 'buffer';
(function($) {
// register namespace
@@ -15,6 +17,7 @@
'Checkmark': CheckmarkFormatter,
'Text': TextFormatter,
'Binary': BinaryFormatter,
+ 'EWKB': EWKBFormatter,
},
},
});
@@ -111,4 +114,39 @@
return '[' + _.escape(value) + ']';
}
}
+
+ function EWKBFormatter(row, cell, value, columnDef, dataContext) {
+ // If column has default value, set placeholder
+ var data = NullAndDefaultFormatter(row, cell, value, columnDef, dataContext);
+ if (data) {
+ return data;
+ } else {
+ let geometry;
+ try {
+ let buffer = new Buffer(value, 'hex');
+ geometry = Geometry.parse(buffer);
+ } catch (e) {
+ //unsupported geometry type
+ return '' +
+ _.escape(value);
+ }
+
+ if (geometry.hasZ) {
+ //the viewer can not render 3d geometry
+ return '' +
+ _.escape(value);
+ }
+ else {
+ return '' +
+ _.escape(value);
+ }
+
+ }
+ }
})(window.jQuery);
diff --git a/web/pgadmin/static/js/sqleditor/geometry_viewer.js b/web/pgadmin/static/js/sqleditor/geometry_viewer.js
new file mode 100644
index 0000000..4fb0e83
--- /dev/null
+++ b/web/pgadmin/static/js/sqleditor/geometry_viewer.js
@@ -0,0 +1,165 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import Alertify from 'pgadmin.alertifyjs';
+import {Geometry} from 'wkx';
+import {Buffer} from 'buffer';
+import BuildGeometryViewerDialog from 'sources/sqleditor/geometry_viewer_dialog';
+
+let GeometryViewer = {
+ 'render_geometry': renderGeometry,
+
+ 'add_header_button': function (columnDefinition) {
+ if (columnDefinition.column_type_internal === 'geometry' ||
+ columnDefinition.column_type_internal === 'geography') {
+ columnDefinition.header = {
+ buttons: [
+ {
+ cssClass: 'div-view-geometry-column',
+ tooltip: 'View all geometries in this column',
+ showOnHover: false,
+ command: 'view-all-geometries',
+ content: '',
+ },
+ ],
+ };
+ }
+ },
+};
+
+function renderGeometry(items, columns, columnIndex) {
+ BuildGeometryViewerDialog();
+ const maxRenderByteLength = 5 * 1024 * 1024; //render geometry data up to 5MB
+ let field = columns[columnIndex].field;
+ let geometries3D = [],
+ supportedGeometries = [],
+ unsupportedItems = [],
+ infoContent = [],
+ geometryItemMap = new Map(),
+ mixedSRID = false,
+ geometryTotalByteLength = 0,
+ tooLargeDataSize = false;
+
+
+ if (_.isUndefined(items)) {
+ Alertify.alert(gettext('Geometry Viewer Error'), gettext('Empty data'));
+ return;
+ }
+
+ if (!_.isArray(items)) {
+ items = [items];
+ }
+
+ if (items.length === 0) {
+ Alertify.alert(gettext('Geometry Viewer Error'), gettext('Empty Column'));
+ return;
+ }
+
+ // parse ewkb data
+ _.every(items, function (item) {
+ try {
+ let value = item[field];
+ let buffer = new Buffer(value, 'hex');
+ let geometry = Geometry.parse(buffer);
+ if (geometry.hasZ) {
+ geometries3D.push(geometry);
+ } else {
+ geometryTotalByteLength += buffer.byteLength;
+ if (geometryTotalByteLength > maxRenderByteLength) {
+ tooLargeDataSize = true;
+ return false;
+ }
+
+ if (!geometry.srid) {
+ geometry.srid = 0;
+ }
+ supportedGeometries.push(geometry);
+ geometryItemMap.set(geometry, item);
+ }
+ } catch (e) {
+ unsupportedItems.push(item);
+ }
+ return true;
+ });
+
+ // generate map info content
+ {
+ if (tooLargeDataSize) {
+ infoContent.push(gettext('Too large data size') +
+ '');
+ }
+ if (geometries3D.length > 0) {
+ infoContent.push(gettext('3D geometry not rendered'));
+ }
+ if (unsupportedItems.length > 0) {
+ infoContent.push(gettext('Unsupported geometry not rendered'));
+ }
+ }
+
+ if (supportedGeometries.length === 0) {
+ Alertify.mapDialog([], 0, undefined, infoContent);
+ return;
+ }
+
+ // group geometries by SRID
+ let geometriesGroupBySRID = _.groupBy(supportedGeometries, 'srid');
+ let SRIDGeometriesPairs = _.pairs(geometriesGroupBySRID);
+ if (SRIDGeometriesPairs.length > 1) {
+ mixedSRID = true;
+ }
+ // select the largest group
+ let selectedPair = _.max(SRIDGeometriesPairs, function (pair) {
+ return pair[1].length;
+ });
+ let selectedSRID = selectedPair[0];
+ let selectedGeometries = selectedPair[1];
+
+ let geoJSONs = _.map(selectedGeometries, function (geometry) {
+ return geometry.toGeoJSON();
+ });
+ let getPopupContent = function (geojson) {
+ let geometry = selectedGeometries[geoJSONs.indexOf(geojson)];
+ let item = geometryItemMap.get(geometry);
+ return itemToTable(item, columns);
+ };
+
+ if (mixedSRID) {
+ infoContent.push(gettext('Mixed SRIDs, current SRID:') + selectedSRID +
+ '');
+ }
+
+ Alertify.mapDialog(geoJSONs, parseInt(selectedSRID), getPopupContent, infoContent);
+}
+
+function itemToTable(item, columns) {
+ let content = '';
+ _.each(columns, function (columnDef) {
+ if (!_.isUndefined(columnDef.display_name)) {
+ content += '| ' + columnDef.display_name + ' | ' + '';
+
+ let value = item[columnDef.field];
+ if (_.isUndefined(value) && columnDef.has_default_val) {
+ content += '[default]';
+ } else if (
+ (_.isUndefined(value) && columnDef.not_null) ||
+ (_.isUndefined(value) || value === null)
+ ) {
+ content += '[null]';
+ } else {
+ content += value;
+ }
+ content += ' |
';
+ }
+ });
+ content += '
';
+ return content;
+}
+
+module.exports = GeometryViewer;
diff --git a/web/pgadmin/static/js/sqleditor/geometry_viewer_dialog.js b/web/pgadmin/static/js/sqleditor/geometry_viewer_dialog.js
new file mode 100644
index 0000000..d609406
--- /dev/null
+++ b/web/pgadmin/static/js/sqleditor/geometry_viewer_dialog.js
@@ -0,0 +1,171 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import Alertify from 'pgadmin.alertifyjs';
+import L from 'leaflet';
+import gettext from 'sources/gettext';
+import $ from 'jquery';
+
+function BuildGeometryViewerDialog() {
+ if (!Alertify.mapDialog) {
+ Alertify.dialog('mapDialog', function () {
+
+ let divContainer;
+ let vectorLayer, osmLayer, lmap, infoControl;
+ const geojsonMarkerOptions = {
+ radius: 4,
+ weight: 3,
+ };
+ const geojsonStyle = {
+ weight: 3,
+ };
+ const popupOption = {
+ closeButton: false,
+ minWidth: 260,
+ maxWidth: 300,
+ maxHeight: 320,
+ };
+
+ return {
+ main: function (geoJSONs, SRID, getPopupContent, infoContent = []) {
+
+ if (infoContent.length > 0) {
+ let content = infoContent.join('
');
+ infoControl.addTo(lmap);
+ infoControl.update(content);
+ }
+
+ if (geoJSONs.length === 0) {
+ lmap.setView([0, 0], 0);
+ return;
+ }
+
+ try {
+ vectorLayer.addData(geoJSONs);
+ } catch (e) {
+ // Invalid LatLng object: (NaN, NaN)
+ lmap.setView([0, 0], 0);
+ return;
+ }
+
+ let bounds = vectorLayer.getBounds();
+ if (!bounds.isValid()) {
+ lmap.setView([0, 0], 0);
+ return;
+ }
+
+ if (typeof getPopupContent === 'function') {
+ let addPopup = function (layer) {
+ layer.bindPopup(function () {
+ return getPopupContent(layer.feature.geometry);
+ }, popupOption);
+ };
+ vectorLayer.eachLayer(addPopup);
+ }
+
+ bounds = bounds.pad(0.1);
+ let maxLength = Math.max(bounds.getNorth() - bounds.getSouth(),
+ bounds.getEast() - bounds.getWest());
+ if (SRID === 4326) {
+ divContainer.addClass('ewkb-viewer-container-plain-background');
+ lmap.options.crs = L.CRS.EPSG3857;
+ lmap.setMinZoom(0);
+ osmLayer.addTo(lmap);
+ } else {
+ lmap.options.crs = L.CRS.Simple;
+ if (maxLength >= 180) {
+ // calculate the min zoom level to enable the map to fit the whole geometry.
+ let minZoom = Math.floor(Math.log2(360 / maxLength)) - 2;
+ lmap.setMinZoom(minZoom);
+ } else {
+ lmap.setMinZoom(0);
+ }
+ }
+
+ if (maxLength > 0) {
+ lmap.fitBounds(bounds);
+ } else {
+ lmap.setView(bounds.getCenter(), 5);
+ }
+ },
+
+ setup: function () {
+ return {
+ options: {
+ closable: true,
+ closableByDimmer: true,
+ maximizable: false,
+ frameless: true,
+ padding: false,
+ overflow: false,
+ title: gettext('Geometry Viewer'),
+ },
+ };
+ },
+
+ build: function () {
+ divContainer = $('');
+ this.elements.content.appendChild(divContainer.get(0));
+ vectorLayer = L.geoJSON([], {
+ style: geojsonStyle,
+ pointToLayer: function (feature, latlng) {
+ return L.circleMarker(latlng, geojsonMarkerOptions);
+ },
+ });
+ osmLayer = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '' +
+ '© OpenStreetMap',
+ });
+ lmap = L.map(divContainer.get(0), {
+ preferCanvas: true,
+ });
+ vectorLayer.addTo(lmap);
+ infoControl = L.control({
+ position: 'topright',
+ });
+ infoControl.onAdd = function () {
+ this._div = L.DomUtil.create('div', 'geometry-viewer-info-control');
+ return this._div;
+ };
+ infoControl.update = function (content) {
+ this._div.innerHTML = content;
+ };
+
+ Alertify.pgDialogBuild.apply(this);
+ this.set('onresized', function () {
+ setTimeout(function () {
+ lmap.invalidateSize();
+ }, 50);
+ });
+
+ this.elements.dialog.style.maxWidth = 'unset';
+ this.elements.dialog.style.width = '80%';
+ this.elements.dialog.style.height = '60%';
+ },
+
+ hooks: {
+ onshow: function () {
+ lmap.invalidateSize();
+ },
+
+ onclose: function () {
+ //reset map
+ lmap.closePopup();
+ infoControl.remove();
+ vectorLayer.clearLayers();
+ divContainer.removeClass('ewkb-viewer-container-plain-background');
+ osmLayer.remove();
+ },
+ },
+ };
+ });
+ }
+}
+
+module.exports = BuildGeometryViewerDialog;
diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
index 75ead26..6bc0a6c 100644
--- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
+++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
@@ -641,3 +641,73 @@ input.editor-checkbox:focus {
.connection-status-hide {
display: none;
}
+
+/* For slickgrid EWKB data viewer button */
+.btn-ewkb-viewer {
+ float: right;
+ position: relative;
+}
+
+/* For EWKB data viewer dialog */
+.ewkb-viewer-container {
+ width: 100%;
+ height: 100%;
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAANElEQVQoU2O8e/fuf2VlZUYGLAAkB5bApggmBteJrAiZjWI0SAJkIrKVxCvAawVeRxLyJgB+Ajc1cwux9wAAAABJRU5ErkJggg==);
+ }
+
+/* For leaflet map background */
+.ewkb-viewer-container-plain-background {
+ background: #ffffff;
+}
+
+/* For geometry column button */
+.div-view-geometry-column {
+ float: right;
+ height: 100%;
+ display: flex;
+ display: -webkit-flex;
+ align-items: center;
+ padding-right: 4px;
+}
+
+/* For leaflet popup */
+.leaflet-popup-content-wrapper {
+ border-radius: 2px;
+}
+
+.leaflet-popup-content {
+ margin: 5px;
+ padding: 10px 10px 0;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+/* For geometry viewer property table */
+.view-geometry-property-table {
+ table-layout: fixed;
+ white-space: nowrap;
+ padding: 0;
+}
+
+.view-geometry-property-table th {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.view-geometry-property-table td {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* For geometry viewer info control */
+.geometry-viewer-info-control {
+ padding: 5px;
+ background: white;
+ border: 2px solid rgba(0, 0, 0, 0.2);
+ background-clip: padding-box;
+ border-radius: 2px;
+}
+
+.geometry-viewer-info-control i{
+ margin: 0 0 0 4px;
+}
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index 1316c7b..a6551fd 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -15,6 +15,7 @@ define('tools.querytool', [
'sources/sqleditor/execute_query',
'sources/sqleditor/query_tool_http_error_handler',
'sources/sqleditor/filter_dialog',
+ 'sources/sqleditor/geometry_viewer',
'sources/history/index.js',
'sourcesjsx/history/query_history',
'react', 'react-dom',
@@ -37,7 +38,7 @@ define('tools.querytool', [
babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror,
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
- HistoryBundle, queryHistory, React, ReactDOM,
+ GeometryViewer, HistoryBundle, queryHistory, React, ReactDOM,
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref) {
/* Return back, this has been called more than once */
@@ -589,6 +590,8 @@ define('tools.querytool', [
- This plugin is useful for selecting rows using checkbox
3) RowSelectionModel
- This plugin is needed by CheckboxSelectColumn plugin to select rows
+ 4) Slick.HeaderButtons
+ - This plugin is useful for add buttons in column header
Grid Options:
-------------
@@ -727,7 +730,13 @@ define('tools.querytool', [
} else if (c.cell == 'binary') {
// We do not support editing binary data in SQL editor and data grid.
options['formatter'] = Slick.Formatters.Binary;
- } else {
+ } else if (c.cell == 'geometry' || c.cell == 'geography'){
+ options['editor'] = is_editable ? Slick.Editors.pgText :
+ Slick.Editors.ReadOnlypgText;
+ // EWKB formatter for viewing geometry data.
+ options['formatter'] = Slick.Formatters.EWKB;
+ }
+ else {
options['editor'] = is_editable ? Slick.Editors.pgText :
Slick.Editors.ReadOnlypgText;
options['formatter'] = Slick.Formatters.Text;
@@ -739,6 +748,13 @@ define('tools.querytool', [
var gridSelector = new GridSelector();
grid_columns = self.grid_columns = gridSelector.getColumnDefinitions(grid_columns);
+ // add 'view' button in geometry and geography type column header
+ _.each(grid_columns, function (c) {
+ if(c.column_type_internal == 'geometry' || c.column_type_internal == 'geography'){
+ GeometryViewer.add_header_button(c);
+ }
+ });
+
if (rows_affected) {
// calculate with for header row column.
grid_columns[0]['width'] = SqlEditorUtils.calculateColumnWidth(rows_affected);
@@ -801,6 +817,14 @@ define('tools.querytool', [
grid.registerPlugin(new ActiveCellCapture());
grid.setSelectionModel(new XCellSelectionModel());
grid.registerPlugin(gridSelector);
+ var headerButtonsPlugin = new Slick.Plugins.HeaderButtons();
+ headerButtonsPlugin.onCommand.subscribe(function (e, args) {
+ let items = args.grid.getData().getItems();
+ let columns = args.grid.getColumns();
+ let columnIndex = columns.indexOf(args.column);
+ GeometryViewer.render_geometry(items, columns, columnIndex);
+ });
+ grid.registerPlugin(headerButtonsPlugin);
var editor_data = {
keys: (_.isEmpty(self.handler.primary_keys) && self.handler.has_oids) ? self.handler.oids : self.handler.primary_keys,
@@ -826,6 +850,16 @@ define('tools.querytool', [
setStagedRows.bind(editor_data));
}
+ // listen for 'view geometry' button click event in datagrid
+ grid.onClick.subscribe(function (e, args) {
+ if ($(e.target).hasClass('btn-view-ewkb-enabled') || $(e.target).parent().hasClass('btn-view-ewkb-enabled')) {
+ var item = dataView.getItem(args.row);
+ var columns = grid.getColumns();
+ var columnIndex = args.cell;
+ GeometryViewer.render_geometry(item, columns, columnIndex);
+ }
+ });
+
grid.onColumnsResized.subscribe(function() {
var columns = this.getColumns();
_.each(columns, function(col) {
@@ -2382,6 +2416,12 @@ define('tools.querytool', [
case 'bytea[]':
col_cell = 'binary';
break;
+ case 'geometry':
+ col_cell = 'geometry';
+ break;
+ case 'geography':
+ col_cell = 'geography';
+ break;
default:
col_cell = 'string';
}
diff --git a/web/regression/javascript/geometry_viewer/geometry_viewer_spec.js b/web/regression/javascript/geometry_viewer/geometry_viewer_spec.js
new file mode 100644
index 0000000..7f113e0
--- /dev/null
+++ b/web/regression/javascript/geometry_viewer/geometry_viewer_spec.js
@@ -0,0 +1,287 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import Alertify from 'pgadmin.alertifyjs';
+import GeometryViewer from 'sources/sqleditor/geometry_viewer';
+import BuildGeometryViewerDialog from 'sources/sqleditor/geometry_viewer_dialog';
+
+describe('geometry viewer test', function () {
+ describe('geometry viewer dialog test', function () {
+ it('should create dialog', function () {
+ expect(Alertify.mapDialog).toBeUndefined();
+ BuildGeometryViewerDialog();
+ expect(Alertify.mapDialog).toBeDefined();
+ });
+
+ });
+
+ describe('geometry viewer add header button test', function () {
+ let add_button = GeometryViewer.add_header_button;
+ it('should add button for geography type', function () {
+ let columnDef = {
+ column_type_internal: 'geography',
+ };
+ add_button(columnDef);
+ expect(columnDef.header).toBeDefined();
+ });
+
+ it('should add button for geometry type', function () {
+ let columnDef = {
+ column_type_internal: 'geometry',
+ };
+ add_button(columnDef);
+ expect(columnDef.header).toBeDefined();
+ });
+
+ it('should do nothing for other type', function () {
+ let columnDef = {
+ column_type_internal: 'integer',
+ };
+ add_button(columnDef);
+ expect(columnDef.header).toBeUndefined();
+ });
+ });
+
+ describe('geometry viewer rener geometry test', function () {
+ let renderGeometry = GeometryViewer.render_geometry;
+ beforeEach(function () {
+ BuildGeometryViewerDialog();
+ spyOn(Alertify, 'mapDialog').and.callFake(function (items) {
+ return items.length;
+ });
+ });
+
+ it('should parse item correctly', function () {
+ // POINT(0 0)
+ let ewkb = '010100000000000000000000000000000000000000';
+ let item = {
+ id: 1,
+ geom: ewkb,
+ };
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(item, columns, columnIndex);
+ expect(Alertify.mapDialog).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not alert dialog', function () {
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(undefined, columns, columnIndex);
+ expect(Alertify.mapDialog).not.toHaveBeenCalled();
+ });
+
+
+ it('should group geometry by srid', function () {
+ // POINT(0 0)
+ let ewkb = '010100000000000000000000000000000000000000';
+ // SRID=32632;POINT(0 0)
+ let ewkb1 = '0101000020787F000000000000000000000000000000000000';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }, {
+ id: 1,
+ geom: ewkb,
+ }, {
+ id: 1,
+ geom: ewkb1,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(2);
+ });
+
+
+ it('should support geometry collection', function () {
+ // GEOMETRYCOLLECTION(POINT(2 3),LINESTRING(2 3,3 4))
+ let ewkb = '01070000000200000001010000000000000000000040000000000000084001' +
+ '02000000020000000000000000000040000000000000084000000000000008400000000' +
+ '000001040';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(1);
+ });
+
+ it('should support geometry M', function () {
+ // SRID=4326;MULTIPOINTM(0 0 0,1 2 1)
+ let ewkb = '0104000060E610000002000000010100004000000000000000000000000000' +
+ '00000000000000000000000101000040000000000000F03F00000000000000400000000' +
+ '00000F03F';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(1);
+ });
+
+ it('should support empty geometry', function () {
+ // GEOMETRYCOLLECTION EMPTY
+ let ewkb = '010700000000000000';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(1);
+ });
+
+
+ it('should support mixed geometry type', function () {
+ // GEOMETRYCOLLECTION EMPTY
+ let ewkb = '010700000000000000';
+ // POINT(0 0)
+ let ewkb1 = '010100000000000000000000000000000000000000';
+ // SRID=4326;MULTIPOINTM(0 0 0,1 2 1)
+ let ewkb2 = '0104000060E610000002000000010100004000000000000000000000000000' +
+ '00000000000000000000000101000040000000000000F03F00000000000000400000000' +
+ '00000F03F';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }, {
+ id: 1,
+ geom: ewkb1,
+ }, {
+ id: 1,
+ geom: ewkb2,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(2);
+ });
+
+
+ it('should not support 3D geometry', function () {
+ // POINT(0 0 0)
+ let ewkb = '0101000080000000000000F03F000000000000F03F000000000000F03F';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(0);
+ });
+
+ it('should not support 3DM geometry', function () {
+ // POINT(0 0 0 0)
+ let ewkb = '01010000C00000000000000000000000000000000000000000000000000000' +
+ '000000000000';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(0);
+ });
+
+ it('should not support TRIANGLE geometry', function () {
+ // TRIANGLE ((0 0, 0 9, 9 0, 0 0))
+ let ewkb = '01110000000100000004000000000000000000000000000000000000000000' +
+ '00000000000000000000000022400000000000002240000000000000000000000000000' +
+ '000000000000000000000';
+ let items = [{
+ id: 1,
+ geom: ewkb,
+ }];
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBe(0);
+ });
+
+ it('should limit data size', function () {
+ // POINT(0 0)
+ let ewkb = '010100000000000000000000000000000000000000';
+ let items = [];
+ for (let i = 0; i < 600000; i++) {
+ items.push({
+ id: i,
+ geom: ewkb,
+ });
+ }
+
+ let columns = [
+ {
+ column_type_internal: 'geometry',
+ field: 'geom',
+ },
+ ];
+ let columnIndex = 0;
+ renderGeometry(items, columns, columnIndex);
+ expect(Alertify.mapDialog.calls.mostRecent().args[0].length).toBeLessThan(600000);
+ });
+ });
+});
diff --git a/web/regression/javascript/geometry_viewer/slickgrid_ewkb_formatter_spec.js b/web/regression/javascript/geometry_viewer/slickgrid_ewkb_formatter_spec.js
new file mode 100644
index 0000000..3dc9b7b
--- /dev/null
+++ b/web/regression/javascript/geometry_viewer/slickgrid_ewkb_formatter_spec.js
@@ -0,0 +1,172 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'sources/slickgrid/formatters';
+
+describe('EWKB formatter test', function () {
+ let EWKBFromatter = window.Slick.Formatters.EWKB;
+ const row = undefined;
+ const cell = undefined;
+
+ describe('format supported geometry', function () {
+ it('should return the view button for geometry', function () {
+ // POINT(0 0)
+ let ewkb = '010100000000000000000000000000000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // LINESTRING(0 0,1 1,1 2)
+ let ewkb = '01020000000300000000000000000000000000000000000000000000000000' +
+ 'F03F000000000000F03F000000000000F03F0000000000000040';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // GEOMETRYCOLLECTION(POINT(2 3),LINESTRING(2 3,3 4))
+ let ewkb = '01070000000200000001010000000000000000000040000000000000084001' +
+ '02000000020000000000000000000040000000000000084000000000000008400000000' +
+ '000001040';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // SRID=32632;POINT(0 0)
+ let ewkb = '0101000020787F000000000000000000000000000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // SRID=4326;MULTIPOINTM(0 0 0,1 2 1)
+ let ewkb = '0104000060E610000002000000010100004000000000000000000000000000' +
+ '00000000000000000000000101000040000000000000F03F00000000000000400000000' +
+ '00000F03F';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // GEOMETRYCOLLECTION(POINT(2 3),LINESTRING(2 3,3 4))
+ let ewkb = '01070000000200000001010000000000000000000040000000000000084001' +
+ '02000000020000000000000000000040000000000000084000000000000008400000000' +
+ '000001040';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // POINT EMPTY
+ let ewkb = '0101000000000000000000F87F000000000000F87F';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // LINESTRING EMPTY
+ let ewkb = '010200000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // POLYGON EMPTY
+ let ewkb = '010300000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // MULTIPOINT EMPTY
+ let ewkb = '010400000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ it('should return the view button for geometry', function () {
+ // GEOMETRYCOLLECTION EMPTY
+ let ewkb = '010700000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('View geometry');
+ });
+ });
+
+ describe('format 3d geometry', function () {
+ it('should return the disabled button for 3d geometry', function () {
+ // POINT(0 0 0)
+ let ewkb = '0101000080000000000000F03F000000000000F03F000000000000F03F';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render 3d geometry');
+ });
+
+ it('should return the disabled button for 3d geometry', function () {
+ // POINT(0 0 0 0)
+ let ewkb = '01010000C00000000000000000000000000000000000000000000000000000' +
+ '000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render 3d geometry');
+ });
+
+ it('should return the disabled button for 3d geometry', function () {
+ let ewkb = '01060000800200000001030000800200000005000000000000000000000000' +
+ '00000000000000000000000000000000000000000010400000000000000000000000000' +
+ '00000000000000000001040000000000000104000000000000000000000000000000000' +
+ '00000000000010400000000000000000000000000000000000000000000000000000000' +
+ '00000000005000000000000000000F03F000000000000F03F0000000000000000000000' +
+ '0000000040000000000000F03F000000000000000000000000000000400000000000000' +
+ '0400000000000000000000000000000F03F000000000000004000000000000000000000' +
+ '00000000F03F000000000000F03F0000000000000000010300008001000000050000000' +
+ '00000000000F0BF000000000000F0BF0000000000000000000000000000F0BF00000000' +
+ '000000C0000000000000000000000000000000C000000000000000C0000000000000000' +
+ '000000000000000C0000000000000F0BF0000000000000000000000000000F0BF000000' +
+ '000000F0BF0000000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render 3d geometry');
+ });
+ });
+
+ describe('format unsupported geometry type', function () {
+ it('should return the disabled button for unsupported geometry', function () {
+ let ewkb = '';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render geometry of this type');
+ });
+ it('should return the disabled button for unsupported geometry', function () {
+ // MULTICURVE( (0 0, 5 5), CIRCULARSTRING(4 0, 4 4, 8 4) )
+ let ewkb = '010B0000000200000001020000000200000000000000000000000000000000' +
+ '00000000000000000014400000000000001440010800000003000000000000000000104' +
+ '00000000000000000000000000000104000000000000010400000000000002040000000' +
+ '0000001040';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render geometry of this type');
+ });
+ it('should return the disabled button for unsupported geometry', function () {
+ // POLYHEDRALSURFACE( ((0 0 0, 0 0 1, 0 1 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 1 0 0, 0 0 0)), ((0 0 0, 1 0 0, 1 0 1, 0 0 1, 0 0 0)), ((1 1 0, 1 1 1, 1 0 1, 1 0 0, 1 1 0)), ((0 1 0, 0 1 1, 1 1 1, 1 1 0, 0 1 0)), ((0 0 1, 1 0 1, 1 1 1, 0 1 1, 0 0 1)) )
+ let ewkb = '010F0000800600000001030000800100000005000000000000000000000000' +
+ '00000000000000000000000000000000000000000000000000000000000000000000000' +
+ '000F03F0000000000000000000000000000F03F000000000000F03F0000000000000000' +
+ '000000000000F03F0000000000000000000000000000000000000000000000000000000' +
+ '00000000001030000800100000005000000000000000000000000000000000000000000' +
+ '0000000000000000000000000000000000000000F03F000000000000000000000000000' +
+ '0F03F000000000000F03F0000000000000000000000000000F03F000000000000000000' +
+ '00000000000000000000000000000000000000000000000000000000000000010300008' +
+ '00100000005000000000000000000000000000000000000000000000000000000000000' +
+ '000000F03F00000000000000000000000000000000000000000000F03F0000000000000' +
+ '000000000000000F03F00000000000000000000000000000000000000000000F03F0000' +
+ '00000000000000000000000000000000000000000000010300008001000000050000000' +
+ '00000000000F03F000000000000F03F0000000000000000000000000000F03F00000000' +
+ '0000F03F000000000000F03F000000000000F03F0000000000000000000000000000F03' +
+ 'F000000000000F03F00000000000000000000000000000000000000000000F03F000000' +
+ '000000F03F0000000000000000010300008001000000050000000000000000000000000' +
+ '000000000F03F00000000000000000000000000000000000000000000F03F0000000000' +
+ '00F03F000000000000F03F000000000000F03F000000000000F03F000000000000F03F0' +
+ '00000000000F03F00000000000000000000000000000000000000000000F03F00000000' +
+ '00000000010300008001000000050000000000000000000000000000000000000000000' +
+ '0000000F03F000000000000F03F0000000000000000000000000000F03F000000000000' +
+ 'F03F000000000000F03F000000000000F03F0000000000000000000000000000F03F000' +
+ '000000000F03F00000000000000000000000000000000000000000000F03F';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render geometry of this type');
+ });
+ it('should return the disabled button for unsupported geometry', function () {
+ // TRIANGLE ((0 0, 0 9, 9 0, 0 0))
+ let ewkb = '01110000000100000004000000000000000000000000000000000000000000' +
+ '00000000000000000000000022400000000000002240000000000000000000000000000' +
+ '000000000000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render geometry of this type');
+ });
+ it('should return the disabled button for unsupported geometry', function () {
+ // TIN( ((0 0 0, 0 0 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 0 0 0)) )
+ let ewkb = '01100000800200000001110000800100000004000000000000000000000000' +
+ '00000000000000000000000000000000000000000000000000000000000000000000000' +
+ '000F03F0000000000000000000000000000F03F00000000000000000000000000000000' +
+ '00000000000000000000000000000000011100008001000000040000000000000000000' +
+ '000000000000000000000000000000000000000000000000000000000000000F03F0000' +
+ '000000000000000000000000F03F000000000000F03F000000000000000000000000000' +
+ '0000000000000000000000000000000000000';
+ expect(EWKBFromatter(row, cell, ewkb)).toContain('Can not render geometry of this type');
+ });
+ });
+});
diff --git a/web/yarn.lock b/web/yarn.lock
index f0ff60b..d3a0384 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -5604,6 +5604,10 @@ lead@^1.0.0:
dependencies:
flush-write-stream "^1.0.2"
+leaflet@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.3.3.tgz#5c8f2fd50e4a41ead93ab850dcd9e058811da9b9"
+
level-codec@~7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-7.0.1.tgz#341f22f907ce0f16763f24bddd681e395a0fb8a7"
@@ -9611,6 +9615,12 @@ window-size@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+wkx@^0.4.5:
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.5.tgz#a85e15a6e69d1bfaec2f3c523be3dfa40ab861d0"
+ dependencies:
+ "@types/node" "*"
+
wordwrap@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"