Actions — Add some custom buttons triggering PostgreSQL queries

Princip

This module allows to add one or several actions in web interface. The concept has been inspired by QGIS actions, which can be used to run scripts inside QGIS.

Example of a feature action:

../../_images/publish-configuration-action-popup.gif

At present, the only engine for Lizmap actions is PostgreSQL queries (Python will not be supported). You can use PostgreSQL and PostGIS power to add specific logic in your web map.

When the user clicks on an action button, a query is sent by Lizmap Web Client to the PostgreSQL database, with the current context data (map extent, feature ID, etc.).

A specific function lizmap_get_data is called with these parameters, and returns a GeoJSON response containing one or several features generated by the SQL query for the given action.

Lizmap Web Client will then run some callbacks from this response:

  • zoom or center to the returned geometry,

  • select features of another layer intersecting the returned geometry,

  • display a message, etc.

Three action scopes are supported :

  • project : an action menu item is added on the left menu bar when the map has at least one project action. The new panel shows an action selector and a button to trigger the selected action.

  • layer: the action selector is shown in the layer Informations panel visible by clicking the (i) button right the the layer name.

  • feature: action buttons are added in the popup toolbar for the layer features, allowing to trigger an action specific to each feature.

Tip

For the project scope, the default database is used (file profiles.ini.php in the configuration chapter.

Example action selector:

../../_images/action-selector.png

Ukázka

You can check the demo about fire hydrants on the demo website.

Klikněte na požární hydrant a

  • buď vyberte budovy, které jsou ve vzdálenosti do 150 m

  • nebo najděte nejbližší hasičskou stanici

Předpoklady

Konfigurace nástroje

At present, the actions cannot be configured from Lizmap plugin in QGIS. A specific JSON configuration file must be written and placed aside the QGIS project in the same directory. This file lists the PostgreSQL actions to be added in the map.

Varování

In Lizmap 3.7, the JSON syntax has changed.

If you use the old JSON syntax, you will have a warning in Lizmap, inviting you to migrate to a newer version of the syntax.

Each action is characterized by a name, a title, a scope, layers, an icon, some optional options,

style, callbacks and confirm property can be used.

  • An action can be proposed for a list of layers: QGIS layer IDs should be used in the layers array

  • An action can have a list of callbacks

Example of a JSON configuration file, name fire_hydrant_actions.qgs.action if the QGIS project file is named fire_hydrant_actions.qgs. In this project, there is a vector layer called Fire hydrants with the internal layer ID emergency_fire_hydrant_04132268_86fb_4d5e_a426_ce3133494091. You can get the QGIS layer internal ID with the QGIS expression @layer_id.

[
    {
        "name": "buffer_150",
        "title": "Buildings in the fire hydrant area (150m)",
        "scope": "feature",
        "layers" : [
            "emergency_fire_hydrant_04132268_86fb_4d5e_a426_ce3133494091"
        ],
        "confirm": "Do you want to select buildings within 150m from this fire hydrant ?",
        "icon": "icon-home",
        "options": {
            "buffer_size": 150,
            "other_param": "yes"
        },
        "style": {
            "graphicName": "circle",
            "pointRadius": 6,
            "fill": true,
            "fillColor": "lightred",
            "fillOpacity": 0.3,
            "stroke": true,
            "strokeWidth": 4,
            "strokeColor": "red",
            "strokeOpacity": 0.8
        },
        "callbacks": [
             {"method": "zoom"},
             {"method": "select", "layerId": "building_90f7692a_0ae2_4a7d_91de_b63cddb92963"}
        ]
    },
    {
        "name": "closest_fire_station",
        "title": "Find the closest fire station from this fire hydrant",
        "scope": "feature",
        "layers" : [
            "emergency_fire_hydrant_04132268_86fb_4d5e_a426_ce3133494091"
        ],
        "confirm": "Do you want to select the closest fire station from this fire hydrant ?",
        "icon": "icon-resize-small",
        "options": {},
        "style": {
            "graphicName": "circle",
            "pointRadius": 6,
            "fill": true,
            "fillColor": "lightred",
            "fillOpacity": 0.3,
            "stroke": true,
            "strokeWidth": 4,
            "strokeColor": "red",
            "strokeOpacity": 0.8
        },
        "callbacks": [
             {"method": "zoom"},
             {"method": "select", "layerId": "stations_1a71d61f_cb99_4ac4_8bd4_86304af9be44"}
        ]
    }
]

The JSON configuration file lists the declared actions.

Každá akce je objekt definovaný:

  • a name, což je identifikátor akce.

  • title, který se používá jako popisek v rozhraní Lizmap

  • a scope which can be: project, layer or feature

  • an icon which is displayed on the action button (See the Bootstrap documentation). An SVG icon can be used instead of a bootstrap icon as a background of the popup action buttons. Use a relative media path (Media).

  • nepovinná vlastnost potvrzení, která obsahuje nějaký text. Pokud je nastavena, zobrazí se uživateli potvrzovací dialogové okno s dotazem, zda má být akce skutečně spuštěna, nebo ne. Použijte ji, pokud akce může změnit některá data v databázi.

  • an options object, giving some additional parameters for this action. You can add any needed parameters. Note that this parameters are hard coded and cannot be changed by the user.

  • a style object allowing to configure the returned geometry style. It follows OpenLayers styling attributes.

  • objekt callbacks umožňuje spustit některé akce po vrácení vygenerované geometrie. Jsou definovány jménem method, které v současnosti může být:

    • zoom: přiblížení vrácené geometrie

    • select: select the features from a given layer intersecting the returned geometry. The target layer QGIS internal ID must be added in the layerId property. In the example, the features of the layer containing buildings, ID building_90f7692a_0ae2_4a7d_91de_b63cddb92963 will be selected.

    • redraw: překreslí (obnoví) danou vrstvu v mapě. ID cílové vrstvy QGIS musí být přidáno do vlastnosti layerId.

How Lizmap uses this configuration file to launch actions

Lizmap detects the presence of this configuration file, and adds the needed logic when the map loads.

For example, for feature scoped actions, when the users clicks on an object of one of the action layer in the map, the popup panel shows the feature data. At the top of each popup item, a toolbar shows one button per each layer action. The action title will be displayed on hovering the action button.

Each button triggers the corresponding action, if it is not yet active (else it deactivates and erases the geometry in the map):

  • Backend Lizmap kontroluje, zda je akce dobře nakonfigurována,

  • creates the PostgreSQL query SELECT public.lizmap_get_data(json) with the parameters written in JSON, and executes it in the layer PostgreSQL database. (See example below)

  • Tento dotaz vrátí GeoJSON, který se poté zobrazí na mapě.

  • If some callbacks have been configured, they are launched (selection, zoom, redraw)

  • A Lizmap event actionResultReceived is emitted with the returned data and action properties. This allow user-defined Javascript scripts to use the action results.

The created PostgreSQL query is built up by Lizmap Web Client and uses the PostgreSQL function lizmap_get_data(json) which must be created beforehand in the PostgreSQL table database. This function also uses a more generic function query_to_geojson(text) which transforms any PostgreSQL query string into a GeoJSON output.

Here is an example below of the query executed in the PostgreSQL database by Lizmap Web Client internally,

  • for the example configuration given above,

  • when the users clicks on the button action buffer_150,

  • for the feature with id 2592251664 of the layer Fire hydrants

  • corresponding to the PostgreSQL table fire_hydrant_actions.emergency_fire_hydrant:

SELECT public.lizmap_get_data('{
    "lizmap_repository": "features",
    "lizmap_project": "fire_hydrant_actions",
    "action_name": "buffer_150",
    "action_scop": "feature",
    "layer_name": "Fire hydrant",
    "layer_schema": "fire_hydrant_actions",
    "layer_table": "emergency_fire_hydrant",
    "feature_id": 2592251664,
    "map_center": "POINT(3.4345918 43.63399141565576)",
    "map_extent": "POLYGON((3.429635077741169 43.63175113378633,3.439548522258832 43.63175113378633,3.439548522258832 43.63623161401291,3.429635077741169 43.63623161401291,3.429635077741169 43.63175113378633))",
    "wkt": "",
    "buffer_size":150,
    "other_param": "yes"
}') AS data;

You can see that Lizmap creates a JSON parameter with all needed information and run the PostgreSQL function lizmap_get_data(text).

The current map extent and map center are also sent as parameters in WKT format (projection EPSG:4326) and can be used in the PostgreSQL function.

Mandatory PostgreSQL functions

You need to create this PostgreSQL functions:

  • query_to_geojson(text) which returns a valid GeoJSON text from any SELECT query

  • lizmap_get_data(text) which is the „control tower“ of Lizmap actions: it creates a specific query for each action based on the parameters and then run the query and returns the GeoJSON

The following SQL code is an example to help you create the needed functions. Obviously, you must adapt them to fit your needs.

-- Returns a valid GeoJSON from any query
CREATE OR REPLACE FUNCTION query_to_geojson(datasource text)
RETURNS json AS
$$
DECLARE
    sqltext text;
    ajson json;
BEGIN
    sqltext:= format('
        SELECT jsonb_build_object(
            ''type'',  ''FeatureCollection'',
            ''features'', jsonb_agg(features.feature)
        )::json
        FROM (
          SELECT jsonb_build_object(
            ''type'',       ''Feature'',
            ''id'',         id,
            ''geometry'',   ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb,
            ''properties'', to_jsonb(inputs) - ''geom''
          ) AS feature
          FROM (
              SELECT * FROM (%s) foo
          ) AS inputs
        ) AS features
    ', datasource);
    RAISE NOTICE 'SQL = %s', sqltext;
    EXECUTE sqltext INTO ajson;
    RETURN ajson;
END;
$$
LANGUAGE 'plpgsql'
IMMUTABLE STRICT;

COMMENT ON FUNCTION query_to_geojson(text) IS 'Generate a valid GEOJSON from a given SQL text query.';

-- Create a query depending on the action, layer and feature and returns a GeoJSON.
CREATE OR REPLACE FUNCTION public.lizmap_get_data(parameters json)
RETURNS json AS
$$
DECLARE
    feature_id varchar;
    layer_name text;
    layer_table text;
    layer_schema text;
    action_name text;
    sqltext text;
    datasource text;
    ajson json;
BEGIN

    action_name:= parameters->>'action_name';
    feature_id:= (parameters->>'feature_id')::varchar;
    layer_name:= parameters->>'layer_name';
    layer_schema:= parameters->>'layer_schema';
    layer_table:= parameters->>'layer_table';

    -- Action buffer_150
    -- Performs a buffer on the geometry
    IF action_name = 'buffer_150' THEN
        datasource:= format('
            SELECT %1$s AS id,
            ''Buildings within 150m of the fire hydrant have been selected'' AS message,
            ST_Buffer(geom, 150) AS geom
            FROM "%2$s"."%3$s"
            WHERE osm_id = ''%1$s''
        ',
        feature_id,
        layer_schema,
        layer_table
        );
    ELSEIF action_name = 'closest_fire_station' THEN
        -- Draw a line to the closest fire station
        datasource:= format('
            WITH tmp_hydrant AS (
                SELECT geom FROM fire_hydrant_actions.emergency_fire_hydrant WHERE osm_id = ''%1$s''
            )
            SELECT
                id, name, ST_Distance(hydrant.geom, stations.geom),
                ''The closest is :  '' || stations.name || '', '' || ST_Distance(hydrant.geom, stations.geom)::integer || ''m, flying air distance'' AS message,
                ST_MakeLine(stations.geom, hydrant.geom) AS geom,
                stations.id AS station_id
            FROM
                fire_hydrant_actions.stations stations,
                tmp_hydrant hydrant
            ORDER BY ST_Distance(hydrant.geom, stations.geom)
            LIMIT 1
        ',
        feature_id
        );
    ELSE
    -- Default : return geometry
        datasource:= format('
            SELECT
            %1$s AS id,
            ''The geometry of the object have been displayed in the map'' AS message
            geom
            FROM "%2$s"."%3$s"
            WHERE id = %1$s
        ',
        feature_id,
        layer_schema,
        layer_table
        );

    END IF;

    SELECT query_to_geojson(datasource)
    INTO ajson
    ;
    RETURN ajson;
END;
$$
LANGUAGE 'plpgsql'
IMMUTABLE STRICT;

COMMENT ON FUNCTION public.lizmap_get_data(json) IS 'Generate a valid GeoJSON from an action described by a name, PostgreSQL schema and table name of the source data, a QGIS layer name, a feature id and additional options.';
  • Jako příklad je zde uvedena funkce lizmap_get_data(json). Jelikož se jedná o klíčový vstupní bod, musíte si ji přizpůsobit svým potřebám. Jejím cílem je vytvořit dotaz pro každý název akce, dynamicky vytvořený pro zadané parametry, a vrátit reprezentaci dat výsledku dotazu ve formátu GeoJSON. Měli byste mít vrácenou pouze jednu funkci: v případě potřeby použijte agregaci. Ve výše uvedeném příkladu použijeme metodu formát pro nastavení textu dotazu a funkci query_to_geojson pro vrácení formátu GeoJSON pro tento dotaz.

  • You can use all the given parameters (action name, source data schema and table name, feature id, QGIS layer name) to create the appropriate query for your action(s), by using the PostgreSQL IF THEN ELSIF ELSE clauses. See the content of the parameters variable in the example above, containing some of the JSON configuration file properties, and some properties of the QGIS layer:

    • Lizmap repository and project keys of the map: lizmap_repository & lizmap_project

    • the action name action_name, for example buffer_150. You should use a simple word with only letters, digits and _,

    • the action scope action_scope, for example feature,

    • QGIS layer name (as in QGIS legend): layer_name, for example Fire hydrant, only for feature actions,

    • the PostgreSQL table schema layer_schema and table name layer_table for the layer, only for feature and layer scoped actions

    • the object feature id feature_id, which corresponds to the value of the primary key field for the popup object, only for feature actions,

    • the other properties given in the JSON configuration file, in the options property, such as buffer_size which is 150 in the example

    • the map center map_center and map extent map_extent

  • Příkaz IF ELSE slouží k provedení jiného dotazu, sestaveného v proměnné datasource, a to kontrolou názvu akce.

  • Pokud vrácená data obsahují pole zpráva, jak je uvedeno v příkladu výše, text obsažený v tomto poli se zobrazí v mapě v bublině se zprávou.

  • Geometrie vrácená funkcí se zobrazí na mapě.

  • You could use your function to edit some data in your database, before returning a GeoJSON. To do so, you need to replace the IMMUTABLE property par VOLATILE. Please USE IT WITH CARE !

Actions and user-defined JavaScript scripts

Since Lizmap Web Client triggers an event actionResultReceived any time the user clicks on an action button, and data is returned (in the same time as the result geometry is drawn on the map), you could use your own JavaScript code to add some logic after the result is shown.

Například zde pouze zapíšeme do konzoly prohlížeče přijatý obsah:

lizMap.events.on({

    actionResultReceived: function(e) {
        // QGIS Layer id
        var layerId = e.layerId;
        console.log('Layer ID = ' + layerId);
        // Feature ID, which means the value of the primary key field
        var featureId = e.featureId;
        console.log('Feature ID = ' + featureId);
        // Action item with its name and other properties: name, title, options, styles, etc.
        var action = e.action;
        console.log('Action properties = ');
        console.log(action);
        // Features returned by the action
        var features = e.features;
        console.log('Returned object = ');
        console.log(features);
    }
});

Tyto údaje můžete libovolně použít v kódu JS.

Actions can also be run from external JavaScript scripts: you can use the actions public methods to run an action, or reset the active action:

// Run an action
lizMap.mainLizmap.action.runLizmapAction(actionName, scope = 'feature', layerId = null, featureId = null, wkt = null);
// Reset the action
lizMap.mainLizmap.action.resetLizmapAction()

A WKT geometry, in EPSG:4326, can also be sent as an additional parameter. This is only possible when running the action with JavaScript. This allows to send a geometry to be used by the PostgreSQL action function lizmap_get_data as a property of the parameters SQL variable. (for example to get data from another table with geometries intersecting this passed WKT geometry)