///////////////////////////////////////////////////////////// // // 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: '', }, ], }; }, 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 = $('
'); 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: '© OpenStreetMap', }), 'Topography': L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { maxZoom: 17, attribution: '© OpenStreetMap,' + ' © SRTM,' + ' © OpenTopoMap', }), 'Gray Style': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap,' + ' © CartoDB', subdomains: 'abcd', maxZoom: 19, }), 'Light Color': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap,' + ' © CartoDB', subdomains: 'abcd', maxZoom: 19, }), 'Dark Matter': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap,' + ' © CartoDB', 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 = '| ' + 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 += '
|---|