Actions - Ajoutez des boutons personnalisés déclenchant des requêtes PostgreSQL

Principe

Ce module permet d’ajouter une ou plusieurs actions dans l’interface web. Le concept a été inspiré par les actions QGIS, qui peuvent être utilisées pour exécuter des scripts dans QGIS.

Exemple d’une action d’entité :

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

À l’heure actuelle, le seul moteur pour les actions Lizmap est les requêtes PostgreSQL (Python ne sera pas pris en charge). Vous pouvez utiliser la puissance de PostgreSQL et PostGIS pour ajouter une logique spécifique à votre application cartographique.

Lorsque l’utilisateur clique sur un bouton d’action, une requête est envoyée par Lizmap Web Client à la base de données PostgreSQL, avec les données de contexte actuelles (étendue de la carte, ID de l’entité, etc.).

Une fonction spécifique lizmap_get_data est appelée avec ces paramètres, et renvoie une réponse GeoJSON contenant une ou plusieurs entités générées par la requête SQL pour l’action donnée.

Lizmap Web Client exécutera ensuite quelques callbacks à partir de cette réponse :

  • zoomer ou centrer sur la géométrie retournée,

  • sélectionner les entités d’une autre couche intersectant la géométrie renvoyée,

  • afficher un message, etc.

Trois contextes d’action sont pris en charge :

  • project : un menu d’action est ajouté dans la barre de menu de gauche lorsque la carte a au moins une action de projet. Le nouveau panneau affiche un sélecteur d’action et un bouton pour déclencher l’action sélectionnée.

  • layer: le sélecteur d’action est affiché dans le panneau d”informations visible en cliquant sur le bouton (i) à droite du nom de la couche.

  • features: des boutons d’action sont ajoutés dans la barre d’outils contextuelle pour les entités de la couche, permettant de déclencher une action spécifique à chaque entité.

Astuce

Pour le contexte project, la base de données par défaut est utilisée (fichier profiles.ini.php dans le chapitre configuration).

Exemple du sélecteur d’action :

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

Démonstration

Vous pouvez vérifier la démo sur les bornes d’incendie sur le site de démonstration.

Cliquez sur une borne incendie et

  • Soit sélectionnez les bâtiments qui sont à 150m

  • Ou trouvez la borne incendie la plus proche

Pré-requis

Configurer l’outil

À l’heure actuelle, les actions ne peuvent pas être configurées à partir de l’extension Lizmap dans QGIS. Un fichier de configuration JSON spécifique doit être rédigé et placé à côté du projet QGIS dans le même répertoire. Ce fichier répertorie les actions PostgreSQL à ajouter dans la carte.

Avertissement

Dans Lizmap 3.7, la syntaxe JSON a changé.

Si vous utilisez l’ancienne syntaxe JSON, vous recevrez un avertissement dans Lizmap, vous invitant à migrer vers une version plus récente de la syntaxe.

Chaque action est caractérisée par un name, un title, un scope, layers, une icon, quelques options facultatives,

les propriétés style, callbacks et confirm peuvent être utilisées.

  • Une action peut être proposée pour une liste de layers : les identifiants de couche QGIS doivent être utilisés dans le tableau layers.

  • Une action peut avoir une liste de``callbacks``

Exemple avec ce fichier de configuration JSON, nom fire_hydrant_actions.qgs.action si le fichier de projet QGIS est nommé fire_hydrant_actions.qgs. Dans ce projet, il existe une couche vectorielle appelée Borne incendie avec l’ID de couche interne emergency_fire_hydrant_04132268_86fb_4d5e_a426_ce3133494091 (vous pouvez obtenir l’ID interne de la couche QGIS avec l’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"}
        ]
    }
]

Le fichier de configuration JSON répertorie les actions déclarées.

Chaque action est un objet défini par :

  • un name qui est l’identifiant de l’action.

  • un title qui est utilisé comme étiquette dans l’interface Lizmap

  • un scope (contexte) qui peut être : project, layer ou feature

  • un icon qui est affiché sur le bouton d’action (Voir la documentation Bootstrap). Un icône SVG peut être utilisé à la place d’une icône bootstrap en tant qu’arrière-plan des boutons d’action de la fenêtre contextuelle. Utilisez un chemin relatif des médias (Média).

  • une propriété optionnelle confirm, contenant du texte. Si elle est définie, une boîte de dialogue de confirmation sera affichée à l’utilisateur pour lui demander si l’action doit réellement être lancée ou non. Utilisez-la si l’action peut modifier certaines données dans votre base de données.

  • un objet options, donnant quelques paramètres supplémentaires pour cette action. Vous pouvez ajouter tous les paramètres nécessaires. Notez que ces paramètres sont codés en dur et ne peuvent pas être modifiés par l’utilisateur.

  • un objet style permettant de configurer le style de géométrie renvoyé. Il suit les attributs de style OpenLayers.

  • un objet callbacks permet de déclencher certaines actions après le retour de la géométrie générée. Ils sont définis par un nom de method, qui peut actuellement être :

    • zoom: zoome vers la géométrie retournée

    • select : sélectionner les entités d’une couche donnée coupant la géométrie renvoyée. La couche cible avec l’identifiant interne QGIS doit être ajoutée dans la propriété layerId. Dans l’exemple, les entités de la couche contenant les bâtiments, l’ID building_90f7692a_0ae2_4a7d_91de_b63cddb92963 sera sélectionné

    • redraw : redessine (rafraîchit) une couche donnée dans la carte. L’ID QGIS de la couche cible doit être ajouté dans la propriété layerId.

Comment Lizmap utilise ce fichier de configuration pour lancer des actions

Lizmap détecte la présence de ce fichier de configuration, et ajoute la logique nécessaire lorsque la carte se charge.

Par exemple, pour les actions features, lorsque l’utilisateur clique sur un objet d’une des couches d’action sur la carte, le panneau contextuel affiche les données de l’entité. En haut de chaque entité du panneau de la popup, une barre d’outils affiche un bouton par action de couche. Le titre de l’action sera affiché en survolant le bouton d’action.

Chaque bouton déclenche l’action correspondante, s’il n’est pas encore actif (sinon il désactive et efface la géométrie dans la carte).

  • l’application Lizmap vérifie si l’action est bien configuré,

  • crée la requête PostgreSQL SELECT public.lizmap_get_data(json) avec les paramètres écrits en JSON, et l’exécute dans la base de données PostgreSQL de la couche. (Voir exemple ci-dessous)

  • Cette requête retourne un GeoJSON qui est ensuite affiché sur la carte.

  • Si des callbacks ont été configurés, ils sont lancés (selection, zoom, redraw).

  • Un événement Lizmap actionResultReceived est émis avec les données renvoyées et les propriétés d’action. Cela permet aux scripts JavaScript définis par l’utilisateur d’utiliser les résultats de l’action.

La requête PostgreSQL créée est construite par Lizmap Web Client et utilise la fonction PostgreSQL lizmap_get_data(json) qui doit être créée au préalable dans la table de la base de données PostgreSQL. Cette fonction utilise également une fonction plus générique query_to_geojson(text) qui transforme n’importe quelle chaîne de requête PostgreSQL en une sortie GeoJSON.

Voici un exemple ci-dessous de la requête exécutée dans la base de données PostgreSQL par Lizmap Web Client en interne,

  • pour la configuration d’exemple donnée ci-dessus,

  • quand les utilisateurs cliquent sur le bouton d’action buffer_150,

  • pour l”entité ayant l’identifiant 2592251664 de la couche Fire hydrants

  • correspondant à la table PostgreSQL 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;

Vous pouvez voir que Lizmap crée des paramètres JSON avec toutes les informations nécessaires et exécute la fonction PostgreSQL lizmap_get_data(text).

L”étendue et le centre de la carte sont également envoyés en tant que paramètres au format WKT (projection EPSG:4326) et peuvent être utilisés dans la fonction PostgreSQL.

Fonctions PostgreSQL obligatoires

Vous devez créer ces fonctions PostgreSQL :

  • query_to_geojson(text) qui renvoie un texte GeoJSON valide à partir de n’importe quelle requête SELECT

  • lizmap_get_data(text) qui est la « tour de contrôle » des actions Lizmap : elle crée une requête spécifique pour chaque action basée sur les paramètres, puis exécute la requête et renvoie le GeoJSON

Le code SQL suivant est un exemple pour vous aider à créer les fonctions nécessaires. Évidemment, vous devez les adapter à vos besoins.

-- 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.';
  • La fonction lizmap_get_data(json) est fournie ici à titre d’exemple. Puisqu’il s’agit du point d’entrée clé, vous devez l’adapter à vos besoins. Il vise à créer une requête pour chaque action, créée dynamiquement pour les paramètres donnés, et à renvoyer une représentation GeoJSON des données par rapport à la requête. Vous devriez avoir une seule entité renvoyée : utilisez l’agrégation si nécessaire. Dans l’exemple ci-dessus, nous utilisons la méthode format pour définir le texte de la requête et la fonction query_to_geojson pour renvoyer le GeoJSON pour cette requête.

  • Vous pouvez utiliser tous les paramètres donnés (nom d’action, schéma de données source et nom de table, identifiant d’entité, nom de couche QGIS) pour créer la requête appropriée pour votre/vos action(s), en utilisant les clauses PostgreSQL IF THEN ELSIF ELSE . Voir le contenu de la variable parameters dans l’exemple ci-dessus, contenant certaines des propriétés du fichier de configuration JSON et certaines propriétés de la couche QGIS :

    • le répertoire et le projet de la carte : lizmap_repository & lizmap_project

    • le nom de l’action action_name, par exemple buffer_500. Vous devez utiliser un mot simple avec uniquement des lettres, des chiffres et _,

    • le contexte de l’action action_scope, par exemple feature,

    • Le nom de la couche QGIS (comme dans la légende QGIS): layer_name, par exemple Fire hydrant, seulement pour les actions feature.

    • le schema PostgreSQL de la table layer_schema et nom de la table layer_table pour cette couche, seulement pour les contextes feature et layer

    • l’objet feature id feature_id, qui correspond à la valeur du champ clé primaire de l’objet popup, seulement pour les actions feature,

    • les autres propriétés données dans le fichier de configuration JSON, dans la propriété options, comme buffer_size qui vaut 150 dans l’exemple

    • le centre de la carte map_center et l’étendue de la carte map_extent

  • Le IF ELSE est utilisé pour faire une requête différente, construite dans la variable datasource, en vérifiant le nom de l’action

  • Si les données de retour contiennent un champ message, comme illustré dans l’exemple ci-dessus, le texte contenu dans ce champ sera affiché dans la carte dans une bulle de message.

  • La géométrie renvoyée par la fonction sera affichée sur la carte.

  • Vous pouvez utiliser votre fonction pour éditer certaines données dans votre base de données, avant de renvoyer un GeoJSON. Pour ce faire, vous devez remplacer la propriété IMMUTABLE par VOLATILE. Veuillez L’UTILISER AVEC PRÉCAUTION !

Actions et scripts JavaScript définis par l’utilisateur

Étant donné que Lizmap Web Client déclenche un événement actionResultReceived à chaque fois que l’utilisateur clique sur un bouton d’action et que les données sont renvoyées (en même temps que la géométrie du résultat est dessinée sur la carte), vous pouvez utiliser votre propre code JavaScript pour ajouter une logique après l’affichage du résultat.

Voir aussi

Chapitre Ajouter votre propre JavaScript

Par exemple, ici nous écrivons simplement dans la console du navigateur le contenu reçu :

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);
    }
});

Vous pouvez utiliser ces données à votre guise dans votre code JS.

Les actions peuvent également être exécutées à partir de scripts JavaScript externes : vous pouvez utiliser les méthodes publiques des actions pour exécuter une action, ou réinitialiser l’action active.

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

Une géométrie WKT, en EPSG:4326, peut également être envoyée en tant que paramètre supplémentaire. Cela n’est possible que lors de l’exécution de l’action avec JavaScript. Cela permet d’envoyer une géométrie à utiliser par la fonction d’action PostgreSQL lizmap_get_data en tant que propriété de la variable SQL parameters. (par exemple pour obtenir des données d’une autre table avec des géométries intersectant cette géométrie WKT transmise)