Dans cet article nous expliquons comment il est possible de mettre en place de la détection de changement (ou change detection) d’objects OSM (nodes, ways ou relation) à l’aide de l’outil Osmose.

Détection de changement de données OSM

Comme OpenStreetMap (OSM) est une base de données qui évolue en permanence (un utilisateur peut, par exemple, ajouter un bâtiment, changer le nom d’un commerce, supprimer une poubelle), il peut être intéressant de vouloir suivre ces changements : si vous tenez un commerce, vous pouvez vouloir être informé des changements des informations qui y sont liées.

Cette problématique est appelée détection de changement (ou change detection) et fait partie du sujet plus général de l' assurance qualité (ou quality assurance). Chez Champs-Libres nous aimons beaucoup ce sujet !

Osmose

Osmose est un des nombreux outils qualité disponibles afin de signaler de possibles problèmes dans les données OSM.

Plus précisément, cet outil fait passer des analyseurs (grosses requêtes sql) qui détectent de possibles erreurs qui sont répertoriées, au final, sur une carte. Dans les erreurs erreurs détectées nous pouvons citer la collision de bâtiments, les sens uniques en cul de sac, …

Toutes ces analyses se font sur une version des données OSM à un moment précis. Il n’y a donc pas de notion d’évolution des données. Nous allons voir comment adapter l’outil pour permettre de traiter cette question.

Avant de parler de l’évolution des données, nous allons aborder le point de la mise à jour des données.

Mise à jour des données via Osmosis et schéma pg_snapshot

Lorsque l’outil osmose fait ses analyses, il le fait sur une copie de la base de données OSM. Pour stocker les données en local, osmose utilise une base de données PostgresSQL avec le schéma pgsnapshot. Ce schéma est une version modifiée et simplifiée du schéma principal de la base de données OSM qui permet, entre autre, la génération de géométries, le stockage des balises dans une seule colonne hstore pour une utilisation et une indexation plus faciles.

Le schéma pgsnapshot contient entre autres 3 tables : nodes, ways et relations qui stockeront ces objets (voir définition du schéma).

Comme les données OSM évoluent en permanence, la base doit être mise à jour régulièrement. L’outil Osmosis est utilisé pour cette tâche.

Une fois la mise à jour des données fait par Osmosis la base contient la dernière version des objets OSM et si des objets ont été supprimés ils n’apparaissent plus.

Pour faire de la détection de changement, nous avons besoin d’enregistrer les modifications et les suppressions des objets OSM.

Voici comment améliorer le schéma de la base de données afin de traiter cette problématique.

Modification du schéma pg_snapshot pour enregistrer les changements

Pour simplifier la présentation, nous allons seulement présenter la manière de gérer les nodes, la manière de gérer les autres objets se déduit facilement de ce cas. L’entièrement de la solution est téléchargeable via ce lien.

Deux nouvelles tables sont rajoutées à la base :

  • nodes_deleted qui va récupérer l’ancienne version des noeuds qui ont été supprimés
  • nodes_history qui va récupérer les mises à jour (seule la dernière version est conservée).

Le code à ajouter :

DROP TABLE IF EXISTS nodes_deleted;
-- cette table récupère l'ancienne version des nodes deleted
-- Cette table est vidée à la fin de la mise à jour d'un changeset
-- par la fonction osmosisUpdate
CREATE TABLE nodes_deleted AS TABLE nodes WITH NO DATA;

DROP TABLE IF EXISTS nodes_history;
CREATE TABLE public.nodes_history
(
    gid SERIAL PRIMARY KEY,
    id bigint NOT NULL,
    version integer NOT NULL,
    user_id integer NOT NULL,
    tstamp timestamp without time zone NOT NULL,
    changeset_id bigint NOT NULL,
    tags hstore,
    visible boolean DEFAULT TRUE,
    geom geometry(Point,4326)
);

DROP TABLE IF EXISTS nodes_history_temp;
-- durant le processus de mise à jour des ways, la première passe
-- met à jour les tags et les noeuds, et le second met à jour
-- la géométrie. Cette table contient les mises à jour et seule la dernière
-- version est conservée.
CREATE TABLE nodes_history_temp AS TABLE nodes WITH NO DATA;
ALTER TABLE nodes_history_temp ADD CONSTRAINT nodes_history_temp_unique_id UNIQUE(id);

Nous créons ensuite une fonction save_deleted_nodes qui sera appelée juste avant la suppression de noeuds lors de la procédure de mise à jour.

CREATE OR REPLACE FUNCTION save_deleted_nodes() RETURNS trigger AS
$$BEGIN
   INSERT INTO nodes_deleted (id, version, user_id, tstamp, changeset_id, tags, geom)
     VALUES (OLD.id, OLD.version, OLD.user_id, OLD.tstamp, OLD.changeset_id, OLD.tags, OLD.geom);
   RETURN OLD;
END;$$ LANGUAGE plpgsql;

Un trigger est défini pour déclencher la fonction save_deleted_nodes quand des nodes sont supprimés durant la mise à jour.

DROP TRIGGER IF EXISTS  save_deleted_nodes_tg ON nodes;
CREATE TRIGGER save_deleted_nodes_tg
    BEFORE DELETE ON nodes
    FOR EACH ROW
    EXECUTE PROCEDURE save_deleted_nodes();

Nous faisons pareil pour garder la version d’un nodes avant sa mise à jour.

CREATE OR REPLACE FUNCTION save_update_nodes() RETURNS trigger AS
$$BEGIN
  -- insert only the first updated version on same update. Subsequent updates will be alteration of geometry or other
  -- stuff dones by osmosis. We want to keep only the first version, which was the previous valid one in the table
  -- nodes.
  INSERT INTO nodes_history_temp (id, version, user_id, tstamp, changeset_id, tags, geom)
    VALUES (OLD.id, OLD.version, OLD.user_id, OLD.tstamp, OLD.changeset_id, OLD.tags, OLD.geom)
    ON CONFLICT ON CONSTRAINT nodes_history_temp_unique_id
      DO NOTHING
    ;
  RETURN NEW;
END;$$ LANGUAGE plpgsql;

Un trigger est déclenché lors d’une mise à jour d’un node.

DROP TRIGGER IF EXISTS save_update_nodes_tg ON nodes;
CREATE TRIGGER save_update_nodes_tg
    BEFORE UPDATE ON nodes
    FOR EACH ROW
    EXECUTE PROCEDURE save_update_nodes();

La dernière étape revient à implémenter la fonction osmosisupdate qui est appelée par OSMOSIS à la fin d’une étape de mise à jour. Nous l’utilisons pour ne garder dans la table nodes_history que la version du node avant sa mise à jour.

-- cette fonction est exécutée par osmosis avant de terminer
-- l'import d'un changeset.
CREATE OR REPLACE FUNCTION public.osmosisupdate(
	)
    RETURNS void
    LANGUAGE 'plpgsql'

    COST 100
    VOLATILE

AS $BODY$
DECLARE
BEGIN
  /* (code for actions) */
  /* (code for ways) */

   - copy the record from nodes_history_temp to nodes_history
  INSERT INTO nodes_history (id, version, user_id, tstamp, changeset_id, tags, visible, geom)
    SELECT id, version, user_id, tstamp, changeset_id, tags, TRUE, geom FROM nodes_history_temp;
  DELETE FROM nodes_history_temp;

  /* (code for relations) */

END;
$BODY$;

Exploitation des données et conclusion

Une fois les modifications faites vous avez deux nouvelles tables : nodes_deleted et nodes_history qui stockent automatiquement une trace des mises à jour des données OSM à chaque mise à jour de la base via Osmosis.

Voici, par exemple, l’historique du node 3704377488 enregistré dans une db de test :

gid  |id        |version|tstamp                 |changeset_id|tags                                                                               |visible|geom                                         |
-----+----------+-------+-------+-----------------------+------------+-----------------------------------------------------------------------------------+-------+---------------------------------------------+
   59|3704377488|      3|2020-10-03 19:35:21.000|    91912685|"railway"=>"crossing", "crossing"=>"traffic_signals", "crossing:barrier"=>"no"     |true   |POINT (2.2401226000000003 48.897935700000005)|
  210|3704377488|      4|2022-03-29 20:34:02.000|   119087786|"railway"=>"tram_crossing", "crossing"=>"traffic_signals", "crossing:barrier"=>"no"|true   |POINT (2.2401226 48.8979357)                 |

Nous venons de voir comment adapter une base OSM pour enregistrer les changements. Cette modification ouvre la voie à de nombreuses possibilités.

L’entièrement de la solution est téléchargeable via ce lien.

Le travail a été effectué par Marc Ducobu, Julien Fastré et Julien Minet et l’article de blog écrit par Marc Ducobu.