diff --git a/TableWidget.jsx b/TableWidget.jsx
new file mode 100644
index 0000000000..5a74058ff0
--- /dev/null
+++ b/TableWidget.jsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require('react');
+const Message = require('../../I18N/Message');
+const loadingState = require('../../misc/enhancers/loadingState');
+const FeatureGrid = loadingState(({ describeFeatureType }) => !describeFeatureType)(require('../../data/featuregrid/FeatureGrid'));
+const InfoPopover = require('./InfoPopover');
+
+const WidgetContainer = require('./WidgetContainer');
+const {
+ Glyphicon,
+ ButtonToolbar,
+ DropdownButton,
+ MenuItem
+} = require('react-bootstrap');
+
+const renderHeaderLeftTopItem = ({ title, description }) => {
+ return title || description ?
Sie können keine Widgets für die ausgewählte Ebene erstellen. Dies liegt wahrscheinlich daran, dass der Layer nicht für die verfügbaren Widgets geeignet ist oder der Server nicht alle benötigten Services oder Informationen zum Generieren eines Widgets bereitstellt. Mögliche Ursachen sind:
" + "noWidgetsAvailableDescription": "
Sie können keine Widgets für die ausgewählte Ebene erstellen. Dies liegt wahrscheinlich daran, dass der Layer nicht für die verfügbaren Widgets geeignet ist oder der Server nicht alle benötigten Services oder Informationen zum Generieren eines Widgets bereitstellt. Mögliche Ursachen sind:
", + "checkAtLeastOneAttribute": "Sie müssen mindestens eine Spalte auswählen" }, "setupFilter": "Konfigurieren Sie einen Filter für die Widgetdaten" }, @@ -1175,6 +1182,9 @@ }, "dashboard": { + "editor": { + "addACardToTheDashboard": "Agregar un widget al tablero" + }, "emptyTitle": "Das Armaturenbrett ist leer" }, "wizard": { diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index 05e5c86257..89367766d8 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -1073,6 +1073,10 @@ "text": { "title": "Text", "caption": "add a text area" + }, + "table": { + "title": "Table", + "caption": "add a table" } }, "selectWidgetType": "Select the widget type", @@ -1096,6 +1100,9 @@ "backToChartOptions": "Back to chart options", "configureChartOptions": "Configure chart options", "configureWidgetOptions": "Configure widget options", + "backToTableOptions": "Back to table options", + "configureTableOptions": "Configure table options", + "resetColumnsSizes": "Reset all changes to the column sizes", "updateWidget": "Update the widget", "addToTheMap": "Add the widget to the map", "titlePlaceholder": "Insert title...", @@ -1103,7 +1110,8 @@ }, "errors": { "noWidgetsAvailableTitle": "No Widgets available", - "noWidgetsAvailableDescription": "
You can't create any widgets for the selected layer. This is probably because the layer is not suitable for the available widgets or the server doesn't expose all the needed services or information to generate widget. Possible causes are:
gs:aggregate process
is not available" + "noWidgetsAvailableDescription": "
You can't create any widgets for the selected layer. This is probably because the layer is not suitable for the available widgets or the server doesn't expose all the needed services or information to generate widget. Possible causes are:
gs:aggregate process
is not available", + "checkAtLeastOneAttribute": "You must select at least one column" }, "setupFilter": "Configure a filter for the widget data" }, @@ -1175,6 +1183,9 @@ }, "dashboard": { + "editor": { + "addACardToTheDashboard": "Add a widget to the dashboard" + }, "emptyTitle": "The dashboard is empty" }, "wizard": { diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES index f4ad3a51ba..eb9488e20d 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -1072,6 +1072,10 @@ "text": { "title": "Texto", "caption": "agregar un área de texto" + }, + "table": { + "title": "Mesa", + "caption": "agregar una mesa" } }, "selectWidgetType": "Seleccione el tipo de widget", @@ -1095,6 +1099,9 @@ "backToChartOptions": "Volver a las opciones de gráfico", "configureChartOptions": "Configurar opciones de gráfico", "configureWidgetOptions": "Configurar las opciones del widget", + "backToTableOptions": "Volver a las opciones de mesa", + "configureTableOptions": "Configurar opciones de tabla", + "resetColumnsSizes": "Restablecer todos los cambios en los tamaños de columna", "updateWidget": "Actualiza el widget", "addToTheMap": "Agrega el widget al mapa", "titlePlaceholder": "Ingrese el título...", @@ -1102,7 +1109,8 @@ }, "errors": { "noWidgetsAvailableTitle": "Sin widgets disponibles", - "noWidgetsAvailableDescription": "
No puede crear ningún widgets para la capa seleccionada. Esto es probablemente porque la capa no es adecuada para los widgets disponibles o el servidor no expone todos los servicios o información necesarios para generar el widget. Las posibles causas son:
" + "noWidgetsAvailableDescription": "
No puede crear ningún widgets para la capa seleccionada. Esto es probablemente porque la capa no es adecuada para los widgets disponibles o el servidor no expone todos los servicios o información necesarios para generar el widget. Las posibles causas son:
", + "checkAtLeastOneAttribute": "Debes seleccionar al menos una columna" }, "setupFilter": "Configurar un filtro para los datos del widget" }, @@ -1174,6 +1182,9 @@ }, "dashboard": { + "editor": { + "addACardToTheDashboard": "Agregar un widget al tablero" + }, "emptyTitle": "El tablero está vacío" }, "wizard": { diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index 7a3065c731..7cc6e44cb4 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -1076,6 +1076,10 @@ "text": { "title": "Texte", "caption": "ajouter une zone de texte" + }, + "table": { + "title": "Table", + "caption": "ajouter une table" } }, "selectWidgetType": "Sélectionnez le type de widget", @@ -1099,6 +1103,9 @@ "backToChartOptions": "retour aux options de graphique", "configureChartOptions": "Configurer les options de graphique", "configureWidgetOptions": "Configurer les options du widget", + "backToTableOptions": "Retour à l'option de table", + "configureTableOptions": "Configurer les options de table", + "resetColumnsSizes": "Réinitialiser toutes les modifications aux tailles de colonnes", "updateWidget": "Mettre à jour le widget", "addToTheMap": "Ajouter le widget à la carte", "titlePlaceholder": "Entrez le titre...", @@ -1106,7 +1113,8 @@ }, "errors": { "noWidgetsAvailableTitle": "Aucun widget disponible", - "noWidgetsAvailableDescription": "
Vous ne pouvez pas créer de widget pour le calque sélectionné. C'est probablement parce que la couche ne convient pas aux widgets ou que le serveur n'expose pas tous les services ou informations nécessaires pour générer un widget. Les causes possibles sont:
" + "noWidgetsAvailableDescription": "
Vous ne pouvez pas créer de widget pour le calque sélectionné. C'est probablement parce que la couche ne convient pas aux widgets ou que le serveur n'expose pas tous les services ou informations nécessaires pour générer un widget. Les causes possibles sont:
", + "checkAtLeastOneAttribute": "Vous devez sélectionner au moins une colonne" }, "setupFilter": "Configurer un filtre pour les données du widget" }, @@ -1177,6 +1185,9 @@ } }, "dashboard": { + "editor": { + "addACardToTheDashboard": "Ajouter un widget au tableau de bord" + }, "emptyTitle": "Le tableau de bord est vide" }, "wizard": { diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index 513338bc7f..6bc55aed59 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -1073,6 +1073,10 @@ "text": { "title": "Testo", "caption": "aggiungi un area di testo" + }, + "table": { + "title": "Tabella", + "caption": "aggiungi una tabella" } }, "selectWidgetType": "Seleziona il tipo di widget", @@ -1096,6 +1100,9 @@ "backToChartOptions": "Torna alle opzioni del grafico", "configureChartOptions": "Configura le opzioni del grafico", "configureWidgetOptions": "Configura le opzioni del widget", + "backToTableOptions": "Torna alle opzioni della tabella", + "configureTableOptions": "Configura le opzioni della tabella", + "resetColumnsSizes": "Reimposta larghezza automatica delle colonne", "updateWidget": "Aggiorna il widget", "addToTheMap": "Aggiungi il widget alla mappa", "titlePlaceholder": "Inserisci il titolo...", @@ -1103,7 +1110,8 @@ }, "errors": { "noWidgetsAvailableTitle": "Nessun widgets disponibile", - "noWidgetsAvailableDescription": "
Non si possono creare widget per il livello selezionato. Questo perchè il livello non è adatto a nessuno degli widget disponibili o perché il server non espone sufficienti servizi per gernerali. Possibili cause:
" + "noWidgetsAvailableDescription": "
Non si possono creare widget per il livello selezionato. Questo perchè il livello non è adatto a nessuno degli widget disponibili o perché il server non espone sufficienti servizi per gernerali. Possibili cause:
", + "checkAtLeastOneAttribute": "seleziona almeno un attributo" }, "setupFilter": "Configura un filtro per il livello selezionato" }, @@ -1175,6 +1183,9 @@ }, "dashboard": { + "editor": { + "addACardToTheDashboard": "Aggiungi un widget alla dashboard" + }, "emptyTitle": "La dashboard è vuota" }, "wizard": { diff --git a/web/client/utils/FeatureGridUtils.js b/web/client/utils/FeatureGridUtils.js index faa01d1397..da913595e6 100644 --- a/web/client/utils/FeatureGridUtils.js +++ b/web/client/utils/FeatureGridUtils.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -const {get, findIndex, isNil} = require('lodash'); +const { get, findIndex, isNil, fill} = require('lodash'); const {getFeatureTypeProperties, isGeometryType, isValid, isValidValueForPropertyName, findGeometryProperty, getPropertyDesciptor} = require('./ogc/WFS/base'); const getGeometryName = (describe) => get(findGeometryProperty(describe), "name"); @@ -77,18 +77,45 @@ const upsertFilterField = (filterFields = [], filter, newObject) => { }; const getAttributeFields = (describe) => (getFeatureTypeProperties(describe) || []).filter( e => !isGeometryType(e)); +// virtual scroll utility functions +const getIdxFarthestEl = (startIdx, pages = [], firstRow, lastRow) => { + return pages.map(val => firstRow <= val && val <= lastRow ? 0 : Math.abs(val - startIdx)).reduce((i, distance, idx, vals) => distance > vals[i] && idx || i, 0); +}; +const removePage = (idxFarthestEl, pages) => pages.filter((el, i) => i !== idxFarthestEl); +const removePageFeatures = (features, idxToRemove, size) => features.filter((el, i) => i < idxToRemove || i >= idxToRemove + size); +const getPagesToLoad = (startPage, endPage, pages, size) => { + let firstMissingPage; + let lastMissingPage; + for (let i = startPage; i <= endPage && firstMissingPage === undefined; i++) { + if (getRowIdx(i * size, pages, size) === -1) { + firstMissingPage = i; + } + } + for (let i = endPage; i >= startPage && lastMissingPage === undefined; i--) { + if (getRowIdx(i * size, pages, size) === -1) { + lastMissingPage = i; + } + } + return [firstMissingPage, lastMissingPage].filter(p => p !== undefined); +}; +// return the options for the paged request to get checking the current cached data +const getCurrentPaginationOptions = ({ startPage, endPage }, oldPages, size) => { + const nPs = getPagesToLoad(startPage, endPage, oldPages, size); + const needPages = (nPs[1] - nPs[0] + 1); + return { startIndex: nPs[0] * size, maxFeatures: needPages * size }; +}; module.exports = { getAttributeFields, featureTypeToGridColumns: ( describe, columnSettings = {}, - {editable=false, sortable=true, resizable=true, filterable = true} = {}, + {editable=false, sortable=true, resizable=true, filterable = true, defaultSize = 200} = {}, {getEditor = () => {}, getFilterRenderer = () => {}, getFormatter = () => {}} = {}) => getAttributeFields(describe).filter(e => !(columnSettings[e.name] && columnSettings[e.name].hide)).map( (desc) => ({ sortable, key: desc.name, - width: columnSettings[desc.name] && columnSettings[desc.name].width || 200, + width: columnSettings[desc.name] && columnSettings[desc.name].width || (defaultSize ? defaultSize : undefined), name: desc.name, resizable, editable, @@ -119,11 +146,12 @@ module.exports = { * @param {function} rowGetter the method to retrieve the feature * @param {object} describe the describe feature type * @param {object} actionOpts some options + * @param {object} columns columns definition * @return {object} The events with the additional parameters */ - getGridEvents: (gridEvents = {}, rowGetter, describe, actionOpts) => Object.keys(gridEvents).reduce((events, currentEventKey) => ({ + getGridEvents: (gridEvents = {}, rowGetter, describe, actionOpts, columns) => Object.keys(gridEvents).reduce((events, currentEventKey) => ({ ...events, - [currentEventKey]: (...args) => gridEvents[currentEventKey](...args, rowGetter, describe, actionOpts) + [currentEventKey]: (...args) => gridEvents[currentEventKey](...args, rowGetter, describe, actionOpts, columns) }), {}), isProperty: (k, d) => !!getPropertyDesciptor(k, d), isValidValueForPropertyName: (v, k, d) => isValidValueForPropertyName(v, getPropertyName(k, d), d), @@ -161,25 +189,45 @@ module.exports = { total: totalFeatures, maxPages: Math.ceil(totalFeatures / maxFeatures) - 1 }), - getRowIdx, - getPagesToLoad: (startPage, endPage, pages, size) => { - let firstMissingPage; - let lastMissingPage; - for (let i = startPage; i <= endPage && firstMissingPage === undefined; i++) { - if (getRowIdx(i * size, pages, size) === -1) { - firstMissingPage = i; - } + getCurrentPaginationOptions, + /** + * updates the pages and the features of the request to support virtual scroll. + * This is virtual scroll with support for + * @param {object} result An object with result --> geoJSON of type "FeatureCollection" + * @param {object} requestOptions contains startPage and endPage needed. + * @param {object} oldData features and pages previously loaded + * @param {object} paginationOptions. The options for pagination `size` and `maxStoredPages`, . + * + */ + updatePages: (result, { endPage, startPage } = {}, { pages, features } = {}, {size, maxStoredPages, startIndex} = {}) => { + const nPs = getPagesToLoad(startPage, endPage, pages, size); + const needPages = (nPs[1] - nPs[0] + 1); + let fts = get(result, "features", []); + if (fts.length !== needPages * size) { + fts = fts.concat(fill(Array(needPages * size - fts.length), false)); } - for (let i = endPage; i >= startPage && lastMissingPage === undefined; i--) { - if (getRowIdx(i * size, pages, size) === -1) { - lastMissingPage = i; + let oldPages = pages; + let oldFeatures = features; + // Cached page should be less than the max of maxStoredPages or the number of page needed to fill the visible are of the grid + const nSpaces = oldPages.length + needPages - Math.max(maxStoredPages, (endPage - startPage + 1)); + if (nSpaces > 0) { + const firstRow = startPage * size; + const lastRow = endPage * size; + // Remove the farthest page from last loaded pages + const averageIdx = firstRow + (lastRow - firstRow) / 2; + for (let i = 0; i < nSpaces; i++) { + const idxFarthestEl = getIdxFarthestEl(averageIdx, pages, firstRow, lastRow); + const idxToRemove = idxFarthestEl * size; + oldPages = removePage(idxFarthestEl, oldPages); + oldFeatures = removePageFeatures(features, idxToRemove, size); } } - return [firstMissingPage, lastMissingPage].filter(p => p !== undefined); - }, - getIdxFarthestEl: (startIdx, pages = [], firstRow, lastRow) => { - return pages.map(val => firstRow <= val && val <= lastRow ? 0 : Math.abs(val - startIdx)).reduce((i, distance, idx, vals) => distance > vals[i] && idx || i, 0); + let pagesLoaded = []; + for (let i = 0; i < needPages; i++) { + pagesLoaded.push(startIndex + (size * i)); + } + return { pages: oldPages.concat(pagesLoaded), features: oldFeatures.concat(fts) }; }, - removePage: (idxFarthestEl, pages) => pages.filter( (el, i) => i !== idxFarthestEl), - removePageFeatures: (features, idxToRemove, size) => features.filter((el, i) => i < idxToRemove || i >= idxToRemove + size) + getRowIdx, + getPagesToLoad }; diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index bec2bf39ce..8e2cfd99e5 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -400,6 +400,14 @@ const LayersUtils = { } return addBaseParams(reqUrl, layer.baseParams || {}); }, + /** + * Gets the layer search url or the current url + * + * @memberof utils.LayerUtils + * @param {Object} layer + * @returns {string} layer url + */ + getSearchUrl: (l = {}) => l.search && l.search.url || l.url, invalidateUnsupportedLayer(layer, maptype) { return isSupportedLayer(layer, maptype) ? checkInvalidParam(layer) : assign({}, layer, {invalid: true}); }, diff --git a/web/client/utils/ogc/WFS/RequestBuilder.js b/web/client/utils/ogc/WFS/RequestBuilder.js index 50f4eb9945..65fe252f67 100644 --- a/web/client/utils/ogc/WFS/RequestBuilder.js +++ b/web/client/utils/ogc/WFS/RequestBuilder.js @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ const filterBuilder = require('../Filter/FilterBuilder'); +const {castArray} = require('lodash'); const {wfsToGmlVersion} = require("./base"); const getStaticAttributesWFS1 = (ver) => 'service="WFS" version="' + ver + '" ' + (ver === "1.0.0" ? 'outputFormat="GML2" ' : "") + @@ -84,9 +85,19 @@ module.exports = function({wfsVersion = "1.1.0", gmlVersion, filterNS, wfsNS="wf + ((startIndex || startIndex === 0) ? ` startIndex="${startIndex}"` : "") + ((maxFeatures || maxFeatures === 0) ? ` ${getMaxFeatures(maxFeatures)}` : ""); }; + const propertyName = (property) => + castArray(property) + .map(p => `<${wfsNS}:PropertyName>${p}${wfsNS}:PropertyName>`) + .join(""); return { + propertyName, ...filterBuilder({gmlVersion: gmlV, wfsVersion, filterNS: filterNS || wfsVersion === "2.0" ? "fes" : "ogc"}), getFeature: (content, opts) => `<${wfsNS}:GetFeature ${requestAttributes(opts)}>${Array.isArray(content) ? content.join("") : content}${wfsNS}:GetFeature>`, + sortBy: (property, order = "ASC") => + `<${wfsNS}:SortBy><${wfsNS}:SortProperty>` + + `${propertyName(property)}` + + `<${wfsNS}:SortOrder>${order}${wfsNS}:SortOrder>` + `${wfsNS}:SortProperty>${wfsNS}:SortBy>`, query: (featureName, content, {srsName ="EPSG:4326"} = {}) => `<${wfsNS}:Query ${wfsVersion === "2.0" ? "typeNames" : "typeName"}="${featureName}" srsName="${srsName}">` + `${Array.isArray(content) ? content.join("") : content}`