Files
pgadmin4/web/pgadmin/static/js/sqleditor/geometry_viewer.js
Aditya Toshniwal f16498a8a7 Optimize Webpack to improve overall performance.
Changes include:
1) Remove underscore-string and sprintf-js packages as we were using only %s. Instead, added a function to do the same. Also changed gettext to behave like sprintf directly.
2) backgrid.sizeable.columns was not used anywhere, removed. @babel/polyfill is deprecated, replaced it with core-js.
3) Moved few css to make sure they get minified and bundled.
4) Added Flask-Compress to send static files as compressed gzip. This will reduce network traffic and improve initial load time for pgAdmin.
5) Split few JS files to make code reusable.
6) Lazy load few modules like leaflet, wkx is required only if geometry viewer is opened. snapsvg loaded only when explain plan is executed. This will improve sqleditor initial opening time.

Reviewed By: Khushboo Vashi
Fixes #4701
2019-10-10 12:05:28 +05:30

436 lines
13 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import {Buffer} from 'buffer';
import $ from 'jquery';
var L = null;
var Geometry = null;
let GeometryViewer = {
panel_closed: true,
go_for_render: function(handler, items, columns, columnIndex) {
let self = this;
if (!self.map_component) {
self.map_component = initMapComponent();
}
if (self.panel_closed) {
let wcDocker = window.wcDocker;
let geometry_viewer_panel = handler.gridView.geometry_viewer =
handler.gridView.docker.addPanel('geometry_viewer',
wcDocker.DOCK.STACKED, handler.gridView.data_output_panel);
$('#geometry_viewer_panel')[0].appendChild(self.map_component.mapContainer.get(0));
self.panel_closed = false;
geometry_viewer_panel.on(wcDocker.EVENT.CLOSED, function () {
$('#geometry_viewer_panel').empty();
self.map_component.clearMap();
self.panel_closed = true;
});
geometry_viewer_panel.on(wcDocker.EVENT.RESIZE_ENDED, function () {
if (geometry_viewer_panel.isVisible()) {
self.map_component.resizeMap();
}
});
geometry_viewer_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function (visible) {
if (visible) {
self.map_component.resizeMap();
} else {
self.map_component.loseFocus();
}
});
}
handler.gridView.geometry_viewer.focus();
self.map_component.clearMap();
let dataObj = parseData(items, columns, columnIndex, Geometry);
self.map_component.renderMap(dataObj);
},
render_geometries: function (handler, items, columns, columnIndex) {
let self = this;
require.ensure(['leaflet', 'wkx'], function(require) {
L = require('leaflet');
Geometry = require('wkx').Geometry;
self.go_for_render(handler, items, columns, columnIndex);
}, function(error){
throw(error);
}, 'geometry');
},
add_header_button: function (columnDefinition) {
columnDefinition.header = {
buttons: [
{
cssClass: 'div-view-geometry-column',
tooltip: 'View all geometries in this column',
showOnHover: false,
command: 'view-geometries',
content: '<button class="btn-xs btn-primary"><i class="fa fa-eye" aria-hidden="true"></i></button>',
},
],
};
},
parse_data: parseData,
};
function initMapComponent() {
const geojsonMarkerOptions = {
radius: 4,
weight: 3,
};
const geojsonStyle = {
weight: 2,
};
const popupOption = {
closeButton: false,
minWidth: 260,
maxWidth: 300,
maxHeight: 300,
};
let mapContainer = $('<div class="geometry-viewer-container"></div>');
let lmap = L.map(mapContainer.get(0), {
preferCanvas: true,
}).setZoom(0);
// update default attribution
lmap.attributionControl.setPrefix('');
let vectorLayer = L.geoJSON([], {
style: geojsonStyle,
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, geojsonMarkerOptions);
},
});
vectorLayer.addTo(lmap);
let baseLayersObj = {
'Empty': L.tileLayer(''),
'Street': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>',
}),
'Topography': L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
{
maxZoom: 17,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
' &copy; <a href="http://viewfinderpanoramas.org" target="_blank">SRTM</a>,' +
' &copy; <a href="https://opentopomap.org" target="_blank">OpenTopoMap</a>',
}),
'Gray Style': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png',
{
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>',
subdomains: 'abcd',
maxZoom: 19,
}),
'Light Color': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}{r}.png',
{
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>',
subdomains: 'abcd',
maxZoom: 19,
}),
'Dark Matter': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png',
{
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>',
subdomains: 'abcd',
maxZoom: 19,
}),
};
let layerControl = L.control.layers(baseLayersObj);
let defaultBaseLayer = baseLayersObj.Street;
let baseLayers = _.values(baseLayersObj);
let 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;
};
let setEPSG3857 = function () {
if (lmap.options.crs !== L.CRS.EPSG3857) {
lmap.options.crs = L.CRS.EPSG3857;
layerControl.addTo(lmap);
lmap.addLayer(defaultBaseLayer);
mapContainer.addClass('geometry-viewer-container-plain-background');
lmap.setMinZoom(0);
}
};
let setSimpleCRS = function () {
if (lmap.options.crs !== L.CRS.Simple) {
lmap.options.crs = L.CRS.Simple;
layerControl.remove();
_.each(baseLayers, function (layer) {
if (lmap.hasLayer(layer)) {
defaultBaseLayer = layer;
layer.remove();
}
});
mapContainer.removeClass('geometry-viewer-container-plain-background');
}
};
setSimpleCRS();
setEPSG3857();
return {
'mapContainer': mapContainer,
'clearMap': function () {
lmap.closePopup();
infoControl.remove();
vectorLayer.clearLayers();
},
'loseFocus': function() {
lmap.fire('blur');
},
'renderMap': function (dataObj) {
let geoJSONs = dataObj.geoJSONs,
SRID = dataObj.selectedSRID,
getPopupContent = dataObj.getPopupContent,
infoList = dataObj.infoList;
let isEmpty = false;
if (geoJSONs.length === 0) {
isEmpty = true;
}
try {
vectorLayer.addData(geoJSONs);
} catch (e) {
// Invalid LatLng object: (NaN, NaN)
infoList.push('An error occurred while rendering data.');
isEmpty = true;
}
let bounds = vectorLayer.getBounds();
if (!bounds.isValid()) {
isEmpty = true;
}
if (infoList.length > 0) {
if (lmap.options.crs === L.CRS.EPSG3857) {
layerControl.remove();
infoControl.addTo(lmap);
layerControl.addTo(lmap);
} else {
infoControl.addTo(lmap);
}
let infoContent = generateInfoContent(infoList);
infoControl.update(infoContent);
}
if (isEmpty) {
setSimpleCRS();
lmap.setView([0, 0], lmap.getZoom());
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) {
setEPSG3857();
} else {
setSimpleCRS();
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(), lmap.getZoom());
}
},
'resizeMap': function () {
setTimeout(function () {
lmap.invalidateSize();
}, 10);
},
};
}
function parseData(items, columns, columnIndex, GeometryLib) {
const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB
const maxRenderGeometries = 100000; // render geometries up to 100000
let field = columns[columnIndex].field;
let geometries3D = [],
supportedGeometries = [],
unsupportedItems = [],
infoList = [],
geometryItemMap = new Map(),
mixedSRID = false,
geometryTotalByteLength = 0,
tooLargeDataSize = false,
tooManyGeometries = false;
if (items.length === 0) {
infoList.push('Empty row.');
return {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': infoList,
};
}
// parse ewkb data
_.every(items, function (item) {
try {
let value = item[field];
let buffer = Buffer.from(value, 'hex');
let geometry = GeometryLib.parse(buffer);
if (geometry.hasZ) {
geometries3D.push(geometry);
} else {
geometryTotalByteLength += buffer.byteLength;
if (geometryTotalByteLength > maxRenderByteLength) {
tooLargeDataSize = true;
return false;
}
if (supportedGeometries.length >= maxRenderGeometries) {
tooManyGeometries = 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 || tooManyGeometries) {
infoList.push(supportedGeometries.length + ' of ' + items.length + ' geometries rendered.');
}
if (geometries3D.length > 0) {
infoList.push(gettext('3D geometries not rendered.'));
}
if (unsupportedItems.length > 0) {
infoList.push(gettext('Unsupported geometries not rendered.'));
}
if (supportedGeometries.length === 0) {
return {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': infoList,
};
}
// 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 = parseInt(selectedPair[0]);
let selectedGeometries = selectedPair[1];
let geoJSONs = _.map(selectedGeometries, function (geometry) {
return geometry.toGeoJSON();
});
let getPopupContent;
if (columns.length >= 3) {
// add popup when geometry has properties
getPopupContent = function (geojson) {
let geometry = selectedGeometries[geoJSONs.indexOf(geojson)];
let item = geometryItemMap.get(geometry);
return itemToTable(item, columns, columnIndex);
};
}
if (mixedSRID) {
infoList.push(gettext('Geometries with non-SRID') + selectedSRID + ' not rendered.');
}
return {
'geoJSONs': geoJSONs,
'selectedSRID': selectedSRID,
'getPopupContent': getPopupContent,
'infoList': infoList,
};
}
function itemToTable(item, columns, ignoredColumnIndex) {
let content = '<table class="table table-bordered table-striped view-geometry-property-table"><tbody>';
// start from 1 because columns[0] is empty
for (let i = 1; i < columns.length; i++) {
if (i !== ignoredColumnIndex) {
let columnDef = columns[i];
content += '<tr><th>' + columnDef.display_name + '</th>';
let value = item[columnDef.field];
if (_.isUndefined(value) && columnDef.has_default_val) {
content += '<td class="td-disabled">[default]</td>';
} else if ((_.isUndefined(value) && columnDef.not_null) ||
(_.isUndefined(value) || value === null)) {
content += '<td class="td-disabled">[null]</td>';
} else {
content += '<td>' + value + '</td>';
}
content += '</tr>';
}
}
content += '</tbody></table>';
return content;
}
function generateInfoContent(infoList) {
let infoContent = infoList.join('<br>');
return infoContent;
}
module.exports = GeometryViewer;