mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
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
436 lines
13 KiB
JavaScript
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: '© <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: '© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
|
|
' © <a href="http://viewfinderpanoramas.org" target="_blank">SRTM</a>,' +
|
|
' © <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: '© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
|
|
' © <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: '© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
|
|
' © <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: '© <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
|
|
' © <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;
|