Skip to content

Commit

Permalink
New Lizmap web component lizmap-features-table to display a compact l…
Browse files Browse the repository at this point in the history
…ist of features

<lizmap-features-table
    layertitle="child sub-districts"
    layerid="sousquartiers_24ceec66_e7fe_46a2_b57a_af5c50389649"
    uniquefield="id"
    expressionfilter="quartmno = 'MC'"
    withgeometry="1"
    sortingfield="libsquart"
    sortingorder="asc"
    draggable="yes"
></lizmap-features-table>
  • Loading branch information
mdouchin committed Jul 31, 2024
1 parent b89e8bd commit 43d40d2
Show file tree
Hide file tree
Showing 15 changed files with 2,626 additions and 85 deletions.
447 changes: 447 additions & 0 deletions assets/src/components/FeaturesTable.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions assets/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Geolocation from './components/Geolocation.js';
import GeolocationSurvey from './components/GeolocationSurvey.js';
import FeaturesTable from './components/FeaturesTable.js';
import SelectionTool from './components/SelectionTool.js';
import SelectionInvert from './components/SelectionInvert.js';
import Snapping from './components/Snapping.js';
Expand Down Expand Up @@ -41,6 +42,7 @@ lizMap.events.on({
uicreated: () => {
window.customElements.define('lizmap-geolocation', Geolocation);
window.customElements.define('lizmap-geolocation-survey', GeolocationSurvey);
window.customElements.define('lizmap-features-table', FeaturesTable);
window.customElements.define('lizmap-selection-tool', SelectionTool);
window.customElements.define('lizmap-selection-invert', SelectionInvert);
window.customElements.define('lizmap-snapping', Snapping);
Expand Down
136 changes: 136 additions & 0 deletions assets/src/modules/FeaturesTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* @module modules/FeaturesTable.js
* @name FeaturesTable
* @copyright 2024 3Liz
* @license MPL-2.0
*/
import { mainLizmap, mainEventDispatcher } from '../modules/Globals.js';
import Utils from './Utils.js';

/**
* @class
* @name FeaturesTable
*/
export default class FeaturesTable {

constructor() {

}

/**
* Get the list of features containing the display expression
*
* @param
* @return {Promise} features - Promise with the JSON list of features
*/


/**
* Get the list of features containing the display expression
*
* @param {string} layerId The QGIS layer ID
* @param {string|null} filter An QGIS expression filter
* @param {boolean} withGeometry If we need to get the geometry
* @param {string|null} fields List of field names separated by comma
*
* @returns — A Promise that resolves with the result of parsing the response body text as JSON.
* @throws — {ResponseError} In case of invalid content type (not application/json or application/vnd.geo+json) or Invalid JSON
* @throws — {HttpError} In case of not successful response (status not in the range 200 – 299)
* @throws — {NetworkError} In case of catch exceptions
*/
getFeatures(layerId, filter = null, withGeometry = false, fields = 'null') {

// Build URL
const url = `${lizUrls.service.replace('service?','features/displayExpression?')}&`;

// Build parameters
let formData = new FormData();
formData.append('layerId', layerId);
formData.append('exp_filter', filter);
formData.append('with_geometry', withGeometry.toString());
formData.append('fields', fields);

// Return promise
return Utils.fetchJSON(url, {
method: "POST",
body: formData
});
}


/**
* Display a lizMap message
*
* @param {string} message Message to display
* @param {string} type Type : error or info
* @param {number} duration Number of millisecond the message must be displayed
*/
addMessage(message, type='info', duration=60000) {

let previousMessage = document.getElementById('lizmap-features-table-message');
if (previousMessage) previousMessage.remove();
mainLizmap.lizmap3.addMessage(
message, type, true, duration
).attr('id', 'lizmap-features-table-message');
}



/**
* Open a Lizmap Popup
*
* @param {string} layerId QGIS layer ID
* @param {object} feature WFS Feature
* @param {string} uniqueField Field containing unique values (used to set the filter for the WMS request)
* @param {HTMLElement} targetElement Target HTML element to display the popup content for the given feature
* @param {callBack} callBack Callback function
*/
openPopup(layerId, feature, uniqueField, targetElement, aCallBack) {

// Get the layer name & configuration
if (!mainLizmap.initialConfig.layers.layerIds.includes(layerId)) {
return null;
}
const layerConfig = mainLizmap.initialConfig.layers.getLayerConfigByLayerId(layerId);
const layerName = layerConfig.name;

// Layer WMS name
const wmsName = layerConfig?.shortname || layerConfig?.name || layerName;

// Filter
const filter = `${wmsName}:"${uniqueField}" = '${feature.properties[uniqueField]}'`;

var crs = 'EPSG:4326';
if(layerConfig.crs && layerConfig.crs != ''){
crs = layerConfig.crs;
}

var wmsOptions = {
'LAYERS': wmsName
,'QUERY_LAYERS': wmsName
,'STYLES': ''
,'SERVICE': 'WMS'
,'VERSION': '1.3.0'
,'CRS': crs
,'REQUEST': 'GetFeatureInfo'
,'EXCEPTIONS': 'application/vnd.ogc.se_inimage'
,'INFO_FORMAT': 'text/html'
,'FEATURE_COUNT': 1
,'FILTER': filter,

};

// Query the server
$.get(globalThis['lizUrls'].service, wmsOptions, function(data) {
// Display the popup in the target element
if (targetElement) {
targetElement.innerHTML = data;
}

// Launch callback
aCallBack(layerId, feature, targetElement);
});
}


}
2 changes: 2 additions & 0 deletions assets/src/modules/Lizmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Config} from './Config.js';
import {State} from './State.js';
import map from './map.js';
import Edition from './Edition.js';
import FeaturesTable from './FeaturesTable.js';
import Geolocation from './Geolocation.js';
import GeolocationSurvey from './GeolocationSurvey.js';
import SelectionTool from './SelectionTool.js';
Expand Down Expand Up @@ -147,6 +148,7 @@ export default class Lizmap {
this.permalink = new Permalink();
this.map = new map();
this.edition = new Edition();
this.featuresTable = new FeaturesTable();
this.geolocation = new Geolocation();
this.geolocationSurvey = new GeolocationSurvey();
this.selectionTool = new SelectionTool();
Expand Down
2 changes: 1 addition & 1 deletion assets/src/modules/Popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export default class Popup {
const sanitizedResponse = DOMPurify.sanitize(response, {
CUSTOM_ELEMENT_HANDLING: {
tagNameCheck: /^lizmap-/,
attributeNameCheck: /crs|bbox|edition-restricted/,
attributeNameCheck: /crs|bbox|edition-restricted|layerid|layertitle|uniquefield|expressionfilter|withgeometry|sortingfield|sortingorder|draggable/,
}
});
lizMap.displayGetFeatureInfo(sanitizedResponse, { x: xCoord, y: yCoord }, evt?.coordinate);
Expand Down
2 changes: 1 addition & 1 deletion assets/src/modules/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default class Tooltip {
if (tooltipLayer) {
this._activeTooltipLayer = tooltipLayer;
} else {
const url = `${lizUrls.service.replace('service','tooltips')}&layerId=${layerTooltipCfg.id}`;
const url = `${lizUrls.service.replace('service?','features/tooltips?')}&layerId=${layerTooltipCfg.id}`;

const vectorStyle = new Style({
image: new Circle({
Expand Down
7 changes: 5 additions & 2 deletions lizmap/modules/lizmap/classes/qgisExpressionUtils.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,12 @@ public static function updateExpressionByUser($layer, $expression, $edition = fa
* @param qgisVectorLayer $layer A QGIS vector layer
* @param array $virtualFields The expressions' list to evaluate as key
* @param string $filter A filter to restrict virtual fields creation
* @param string $withGeometry 'true' to get geometries of features
* @param string $fields A list of field names separated by comma. E.g. 'name,code'
*
* @return null|array the features with virtual fields
*/
public static function virtualFields($layer, $virtualFields, $filter = null)
public static function virtualFields($layer, $virtualFields, $filter = null, $withGeometry = 'true', $fields = '')
{
// Evaluate the expression by qgis
$project = $layer->getProject();
Expand All @@ -167,7 +169,8 @@ public static function virtualFields($layer, $virtualFields, $filter = null)
'map' => $project->getRelativeQgisPath(),
'layer' => $layer->getName(),
'virtuals' => json_encode($virtualFields),
'with_geometry' => 'true',
'with_geometry' => $withGeometry,
'fields' => $fields,
);
if ($filter) {
$params['filter'] = $filter;
Expand Down
185 changes: 185 additions & 0 deletions lizmap/modules/lizmap/controllers/features.classic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php
/**
* Get features from QGIS Server with the help of expressions.
*
* @author 3liz
* @copyright 2024 3liz
*
* @see https://3liz.com
*
* @license Mozilla Public License : https://www.mozilla.org/MPL/
*/
class featuresCtrl extends jController
{
/**
* Get all tooltips of a given layer.
*
* @urlparam $REPOSITORY Name of the repository
* @urlparam $PROJECT Name of the project
* @urlparam $LAYERID Layer Id
*
* @return jResponseJson geoJSON content
*/
public function tooltips()
{
/** @var jResponseJson $rep */
$rep = $this->getResponse('json');
$content = array();
$rep->data = $content;

// Get project and repository, and check rights
$project = $this->param('project');
$repository = $this->param('repository');
$layerId = trim($this->param('layerId', ''));
$lproj = null;

try {
$lproj = lizmap::getProject($repository.'~'.$project);
if (!$lproj) {
jMessage::add('The lizmap project '.strtoupper($project).' does not exist !', 'ProjectNotDefined');

return $rep;
}
} catch (\Lizmap\Project\UnknownLizmapProjectException $e) {
jLog::logEx($e, 'error');
jMessage::add('The lizmap project '.strtoupper($project).' does not exist !', 'ProjectNotDefined');

return $rep;
}
if (!$lproj->checkAcl()) {
jMessage::add(jLocale::get('view~default.repository.access.denied'), 'AuthorizationRequired');

return $rep;
}

$qgisLayer = $lproj->getLayer($layerId);

if (!$qgisLayer) {
jMessage::add('The layer Id '.$layerId.' does not exist !', 'error');

return $rep;
}

$tooltipLayers = $lproj->getTooltipLayers();
$layerName = $qgisLayer->getName();

if (isset($tooltipLayers->{$layerName}, $tooltipLayers->{$layerName}->{'template'})) {
$tooltip = array('tooltip' => $tooltipLayers->{$layerName}->{'template'});

$data = \qgisExpressionUtils::replaceExpressionText(
$qgisLayer,
$tooltip,
);

$rep->data = $data;
}

return $rep;
}

/**
* Get display expressions evaluated for the given layer and parameters.
*
* @urlparam string $REPOSITORY Name of the repository
* @urlparam string $PROJECT Name of the project
* @urlparam string $LAYERID Layer Id
* @urlparam string $EXP_FILTER QGIS expression filter
* @urlparam string $WITH_GEOMETRY If we need to get the features geometries
* @urlparam string $FIELDS List of field names separated by comma
*
* @return jResponseJson geoJSON content
*/
public function displayExpression()
{
/** @var jResponseJson $rep */
$rep = $this->getResponse('json');
$content = array(
'status' => 'error',
'data' => null,
'error' => 'An unexpected error occurred preventing to fetch the data',
);
$rep->data = $content;

// Get project and repository, and check rights
$project = $this->param('project');
$repository = $this->param('repository');
$layerId = trim($this->param('layerId', ''));
$withGeometry = trim($this->param('with_geometry', 'false'));
if (!in_array(strtolower($withGeometry), array('true', 'false'))) {
$withGeometry = 'false';
}
$fields = trim($this->param('fields', 'null'));
$lproj = null;

try {
$lproj = lizmap::getProject($repository.'~'.$project);
if (!$lproj) {
$content['error'] = 'The lizmap project '.strtoupper($project).' does not exist !';
$rep->data = $content;

return $rep;
}
} catch (\Lizmap\Project\UnknownLizmapProjectException $e) {
jLog::logEx($e, 'error');
$content['error'] = 'The lizmap project '.strtoupper($project).' does not exist !';
$rep->data = $content;

return $rep;
}
if (!$lproj->checkAcl()) {
$content['error'] = jLocale::get('view~default.repository.access.denied');
$rep->data = $content;

return $rep;
}

/** @var null|\qgisVectorLayer $qgisLayer */
$qgisLayer = $lproj->getLayer($layerId);

if ($qgisLayer === null) {
$content['error'] = 'The layer Id '.$layerId.' does not exist !';
$rep->data = $content;

return $rep;
}

// Use Lizmap server plugin to evaluate the display expression & feature ID
$expressions = array(
// Get feature id
'feature_id' => '@id',
// Get display expression
'display_expression' => 'display_expression()',
);

// Filter
$exp_filter = trim($this->param('exp_filter', 'FALSE'));

// Get the evaluated features for the given layer and parameters
$getDisplayExpressions = qgisExpressionUtils::virtualFields(
$qgisLayer,
$expressions,
$exp_filter,
$withGeometry,
$fields
);

// If the returned content is null, an error occurred
// while getting the data from QGIS Server lizmap plugin
if ($getDisplayExpressions === null) {
$content['error'] = 'An error occurred while getting the display name for the features !';
$rep->data = $content;

return $rep;
}

// Send the data on success
$content = array(
'status' => 'success',
'data' => $getDisplayExpressions,
'error' => null,
);
$rep->data = $content;

return $rep;
}
}
Loading

0 comments on commit 43d40d2

Please sign in to comment.