pgadmin4/web/pgadmin/static/js/sqleditor/geometry_viewer.js
Libor M 6a9af07f7a Fixed gettext usage:
- fixed gettext usage with .format() only for original text with %s
  - fixed typos
  - fixed translation yes/no buttons in the dialog.
  - improved translating sentences without "connecting" words (eg. see web/pgadmin/dashboard/static/js/dashboard.js,
     word 'cancel' needs to be translated in the Czech language as 'zrušit' but in another sentence as 'zrušení')
  - added gettext for text translations.
2020-04-10 14:52:41 +05:30

436 lines
13 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, 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 %s not rendered.', selectedSRID));
}
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;