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 += '' + ''; + } + }); + 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 += '
'; + 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"