Skip to content

Commit e07738e

Browse files
Mise à jour des AOMs 2025 (#4914)
1 parent dccbe3e commit e07738e

File tree

7 files changed

+169
-92
lines changed

7 files changed

+169
-92
lines changed

apps/transport/lib/db/aom.ex

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ defmodule DB.AOM do
1111
alias Geo.MultiPolygon
1212

1313
typed_schema "aom" do
14-
# composition_res_id matches the id_reseau attribute from the Cerema dataset it’s the official ID of the AOM
15-
field(:composition_res_id, :integer)
1614
field(:insee_commune_principale, :string)
1715
field(:siren, :string)
1816
field(:nom, :string)
@@ -31,6 +29,4 @@ defmodule DB.AOM do
3129
@spec get(insee_commune_principale: binary()) :: __MODULE__ | nil
3230
def get(insee_commune_principale: nil), do: nil
3331
def get(insee_commune_principale: insee), do: Repo.get_by(AOM, insee_commune_principale: insee)
34-
35-
def created_after_2021?(%__MODULE__{composition_res_id: composition_res_id}), do: composition_res_id >= 1_000
3632
end

apps/transport/lib/db/commune.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule DB.Commune do
44
"""
55
use Ecto.Schema
66
use TypedEctoSchema
7-
alias DB.{AOM, Departement, EPCI, Region}
7+
alias DB.{Departement, EPCI, Region}
88
alias Geo.MultiPolygon
99

1010
typed_schema "commune" do
@@ -15,9 +15,8 @@ defmodule DB.Commune do
1515
field(:population, :integer)
1616
field(:siren, :string)
1717
field(:arrondissement_insee, :string)
18+
field(:aom_siren, :string)
1819

19-
# In theory a commune has only one AOM, the reference is done through the composition_res_id attribute
20-
belongs_to(:aom_res, AOM, references: :composition_res_id)
2120
belongs_to(:region, Region)
2221
belongs_to(:departement, Departement, foreign_key: :departement_insee, references: :insee, type: :string)
2322
belongs_to(:epci, EPCI, foreign_key: :epci_insee, references: :insee, type: :string)

apps/transport/lib/mix/tasks/transport/import_aoms.ex

Lines changed: 70 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
1818

1919
@shortdoc "Refreshes the database table `aom` with the latest data"
2020
use Mix.Task
21-
import Ecto.{Query}
21+
import Ecto.Query
2222
alias DB.{AOM, Commune, Region, Repo}
2323
require Logger
2424

@@ -27,12 +27,12 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
2727
# download the Cerema file (.ods)
2828
# rename columns that are on two lines
2929
# export as CSV and publish as community resource
30-
@aom_file "https://static.data.gouv.fr/resources/liste-et-composition-des-autorites-organisatrices-de-la-mobilite-aom/20241108-105224/liste-aoms-2024.csv"
30+
@aom_file "https://www.data.gouv.fr/api/1/datasets/r/2e89a90a-6ca5-445b-b6c2-6f49cdeb8a2d"
3131
# Same for composition of each AOM, but no need even to rename columns
32-
@aom_insee_file "https://static.data.gouv.fr/resources/liste-et-composition-des-autorites-organisatrices-de-la-mobilite-aom/20241122-154942/composition-communale-aom-2024.csv"
32+
@aom_insee_file "https://www.data.gouv.fr/api/1/datasets/r/5195d58d-572e-4f13-8477-bbb11da42b93"
3333

3434
# We don’t add collectivité d’outremer de Saint-Martin
35-
@ignored_aom_ids ["312"]
35+
@ignored_aom_sirens ["219711272"]
3636

3737
@spec to_int(binary()) :: number() | nil
3838
def to_int(""), do: nil
@@ -58,13 +58,12 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
5858
Logger.info("aom #{nom} || previous region #{aom.region.nom} --- #{new_region.nom}")
5959
end
6060

61-
external_id = to_int(line["Id réseau"])
61+
siren = line["N° SIREN"] |> String.trim()
6262

63-
{external_id,
63+
{siren,
6464
Ecto.Changeset.change(aom, %{
65-
composition_res_id: external_id,
6665
departement: extract_departement_insee(line["Département"]),
67-
siren: line["N° SIREN"] |> String.trim(),
66+
siren: siren,
6867
nom: nom,
6968
forme_juridique: normalize_forme(line["Forme juridique"]),
7069
# This is inconsistent with the real number of communes for some lines
@@ -91,6 +90,7 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
9190
defp normalize_forme("CC"), do: "Communauté de communes"
9291
defp normalize_forme("METRO"), do: "Métropole"
9392
defp normalize_forme("PETR"), do: "Pôle d'équilibre territorial et rural"
93+
defp normalize_forme("POLEM"), do: "Pôle Métropolitain"
9494
defp normalize_forme(f), do: f
9595

9696
@spec normalize_nom(binary()) :: binary()
@@ -109,11 +109,11 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
109109
old_aoms =
110110
AOM
111111
|> Repo.all()
112-
|> Map.new(fn aom -> {aom.composition_res_id, aom} end)
112+
|> Map.new(fn aom -> {aom.siren, aom} end)
113113

114114
# get all the aom to import, outside of the transaction to reduce the time in the transaction
115115
# this already builds the changeset
116-
# Mapset of {composition_res_id, changeset}
116+
# Mapset of {aom_siren, changeset}
117117
aoms_to_add = get_aom_to_import() |> Enum.map(&changeset/1) |> MapSet.new()
118118

119119
display_changes(old_aoms, aoms_to_add)
@@ -131,7 +131,7 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
131131
import_insee_aom()
132132
enable_trigger()
133133
end,
134-
timeout: 1_000_000
134+
timeout: :infinity
135135
)
136136

137137
# we can then compute the aom geometries (the union of each cities geometries)
@@ -153,12 +153,12 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
153153
|> IO.binstream(:line)
154154
|> CSV.decode(separator: ?,, headers: true, validate_row_length: true)
155155
|> Enum.map(fn {:ok, line} -> line end)
156-
|> Enum.reject(fn line -> line["Id réseau"] in (["", nil] ++ @ignored_aom_ids) end)
156+
|> Enum.reject(fn line -> line["N° SIREN"] in (["", nil] ++ @ignored_aom_sirens) end)
157157
end
158158

159159
defp existing_or_new_aom(line) do
160160
AOM
161-
|> Repo.get_by(composition_res_id: to_int(line["Id réseau"]))
161+
|> Repo.get_by(nom: normalize_nom(String.trim(line["Nom"])))
162162
|> case do
163163
nil ->
164164
%AOM{}
@@ -176,14 +176,14 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
176176
defp delete_old_aoms(aom_added, old_aoms) do
177177
Logger.info("deleting removed aom")
178178

179-
composition_res_id_added =
179+
ids_added =
180180
aom_added
181181
|> Enum.map(fn {id, _changeset} -> id end)
182182
|> MapSet.new()
183183

184184
old_aoms
185-
|> Enum.each(fn {composition_res_id, old_aom} ->
186-
unless MapSet.member?(composition_res_id_added, composition_res_id) do
185+
|> Enum.each(fn {aom_siren, old_aom} ->
186+
unless MapSet.member?(ids_added, aom_siren) do
187187
Logger.info("trying to delete old aom: #{old_aom.id} - #{old_aom.nom}")
188188

189189
# Note: if the delete is impossible, you need to find what still depend on this aom,
@@ -194,7 +194,7 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
194194
end
195195

196196
defp import_insee_aom do
197-
Logger.info("Linking aoms to cities")
197+
Logger.info("Linking AOMs to cities")
198198

199199
{:ok, %HTTPoison.Response{status_code: 200, body: body}} =
200200
HTTPoison.get(@aom_insee_file, [], hackney: [follow_redirect: true])
@@ -204,15 +204,15 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
204204
stream
205205
|> IO.binstream(:line)
206206
|> CSV.decode(separator: ?,, headers: true, validate_row_length: true)
207-
|> Enum.map(fn {:ok, line} -> {line["N° INSEE"], line["Id réseau"]} end)
208-
|> Enum.reject(fn {_insee, id_reseau} -> id_reseau == "" || id_reseau == "-" end)
209-
|> Enum.flat_map(fn {insee, id_reseau} ->
207+
|> Enum.map(fn {:ok, line} -> {line["N° INSEE"], line["N° SIREN AOM"]} end)
208+
|> Enum.reject(fn {_insee, siren_reseau} -> siren_reseau == "" || siren_reseau == "-" end)
209+
|> Enum.flat_map(fn {insee, siren_reseau} ->
210210
# To reduce the number of UPDATE in the DB, we first check which city needs to be updated
211211
Commune
212-
|> where([c], c.insee == ^insee and (c.aom_res_id != ^id_reseau or is_nil(c.aom_res_id)))
212+
|> where([c], c.insee == ^insee and (c.aom_siren != ^siren_reseau or is_nil(c.aom_siren)))
213213
|> select([c], c.id)
214214
|> Repo.all()
215-
|> Enum.map(fn c -> {c, id_reseau} end)
215+
|> Enum.map(fn c -> {c, siren_reseau} end)
216216
end)
217217
|> Enum.reduce(%{}, fn {commune, aom}, commune_by_aom ->
218218
# Then we group those city by AO, to only do one UPDATE query for several cities
@@ -222,7 +222,7 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
222222
|> Enum.map(fn {aom, list_communes} ->
223223
Commune
224224
|> where([c], c.id in ^list_communes)
225-
|> Repo.update_all(set: [aom_res_id: aom])
225+
|> Repo.update_all(set: [aom_siren: aom])
226226
end)
227227
end
228228

@@ -240,16 +240,16 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
240240
SELECT
241241
ST_UNION(commune.geom)
242242
FROM commune
243-
WHERE commune.aom_res_id = ?
243+
WHERE commune.aom_siren = ?
244244
)
245245
""",
246-
a.composition_res_id
246+
a.siren
247247
)
248248
]
249249
]
250250
),
251251
[],
252-
timeout: 1_000_000
252+
timeout: :infinity
253253
)
254254
end
255255

@@ -258,54 +258,38 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
258258

259259
max_for_each_aom =
260260
from(c in DB.Commune,
261-
where: not is_nil(c.aom_res_id),
262-
group_by: c.aom_res_id,
263-
select: %{aom_res_id: c.aom_res_id, max_population: max(c.population)}
261+
where: not is_nil(c.aom_siren),
262+
group_by: c.aom_siren,
263+
select: %{aom_siren: c.aom_siren, max_population: max(c.population)}
264264
)
265265

266266
main_communes =
267267
from(c in DB.Commune,
268-
where: not is_nil(c.aom_res_id),
268+
where: not is_nil(c.aom_siren),
269269
join: max_for_each_aom in subquery(max_for_each_aom),
270-
on: c.aom_res_id == max_for_each_aom.aom_res_id and c.population == max_for_each_aom.max_population,
271-
select: [c.aom_res_id, c.insee]
270+
on: c.aom_siren == max_for_each_aom.aom_siren and c.population == max_for_each_aom.max_population,
271+
select: [c.aom_siren, c.insee]
272272
)
273273

274274
main_communes =
275275
main_communes
276276
|> DB.Repo.all()
277-
|> MapSet.new(fn [aom_res_id, insee] -> {aom_res_id, insee} end)
277+
|> MapSet.new(fn [aom_siren, insee] -> {aom_siren, insee} end)
278278

279279
{:ok, _} =
280280
Repo.transaction(
281281
fn ->
282-
disable_trigger()
283-
284282
main_communes
285-
|> Enum.each(fn {aom_res_id, insee} ->
283+
|> Enum.each(fn {aom_siren, insee} ->
286284
AOM
287-
|> Repo.get_by!(composition_res_id: aom_res_id)
288-
|> Ecto.Changeset.change(%{insee_commune_principale: insee})
289-
|> Repo.update()
285+
|> where([a], a.siren == ^aom_siren)
286+
|> Repo.update_all(set: [insee_commune_principale: insee])
290287
end)
291-
292-
enable_trigger()
293288
end,
294-
timeout: 1_000_000
289+
timeout: :infinity
295290
)
296291
end
297292

298-
defp disable_trigger do
299-
Repo.query!("ALTER TABLE aom DISABLE TRIGGER refresh_places_aom_trigger;")
300-
Repo.query!("ALTER TABLE commune DISABLE TRIGGER refresh_places_commune_trigger;")
301-
end
302-
303-
defp enable_trigger do
304-
Repo.query!("ALTER TABLE aom ENABLE TRIGGER refresh_places_aom_trigger;")
305-
Repo.query!("ALTER TABLE commune ENABLE TRIGGER refresh_places_commune_trigger;")
306-
Repo.query!("REFRESH MATERIALIZED VIEW places;")
307-
end
308-
309293
defp migrate_datasets_to_new_aoms do
310294
queries = """
311295
-- This could be mostly automatized, you just have to look for a commune of the old AOM and see where it was migrated.
@@ -336,47 +320,71 @@ defmodule Mix.Tasks.Transport.ImportAOMs do
336320
--
337321
-- 2024
338322
-- Migrates a dataset to Pôle Métropolitain Mobilités Le Mans – Sarthe
339-
update dataset_aom_legal_owner set aom_id = (select id from aom where composition_res_id = 1293) where aom_id IN (1283, 1285, 1288, 1292, 1294);
323+
-- update dataset_aom_legal_owner set aom_id = (select id from aom where composition_res_id = 1293) where aom_id IN (1283, 1285, 1288, 1292, 1294);
324+
-- 2025
325+
-- Datasets still associated with deleted AOM as territory
326+
-- %{
327+
-- 26 => [[26, "256900994", 1217], [26, "256900994", 871], [26, "256900994", 1216], [26, "256900994", 1259], [26, "256900994", 1139], [26, "256900994", 755], [26, "256900994", 695], [26, "256900994", 318]],
328+
-- 66 => [[66, "200011773", 1179], [66, "200011773", 164]],
329+
-- 168 => [[168, "246700967", 531]],
330+
-- 263 => [[263, "200016632", 940]],
331+
-- 1047 => [[1047, "243800984", 742]],
332+
-- 1301 => [[1301, "200070464", 1123]]
333+
-- }
334+
update dataset set aom_id = (select id from aom where siren = '200096386') where aom_id = 26;
335+
update dataset set aom_id = (select id from aom where siren = '200075372') where aom_id = 66;
336+
update dataset set aom_id = (select id from aom where siren = '200069680') where aom_id = 168;
337+
update dataset set aom_id = (select id from aom where nom = 'Région Auvergne-Rhône-Alpes (CC du Bassin d''Aubenas)') where aom_id = 263;
338+
update dataset set aom_id = (select id from aom where siren = '253800825') where aom_id = 1047;
339+
update dataset set aom_id = (select id from aom where nom = 'Région Auvergne-Rhône-Alpes (CC Coeur de Maurienne Arvan)') where aom_id = 1301;
340340
"""
341341

342342
queries |> String.split(";") |> Enum.each(&Repo.query!/1)
343343
end
344344

345+
defp disable_trigger do
346+
DB.Repo.query!("ALTER TABLE aom DISABLE TRIGGER aom_update_trigger;")
347+
end
348+
349+
defp enable_trigger do
350+
DB.Repo.query!("ALTER TABLE aom ENABLE TRIGGER aom_update_trigger;")
351+
end
352+
345353
defp display_changes(old_aoms, aoms_to_add) do
346354
mapset_first_elem_diff = fn a, b ->
347355
a |> MapSet.new(&elem(&1, 0)) |> MapSet.difference(b |> MapSet.new(&elem(&1, 0)))
348356
end
349357

350358
new_aoms = mapset_first_elem_diff.(aoms_to_add, old_aoms)
351359
removed_aoms = mapset_first_elem_diff.(old_aoms, aoms_to_add)
352-
Logger.info("#{new_aoms |> Enum.count()} new AOMs. reseau_id codes: #{Enum.join(new_aoms, ", ")}")
353-
Logger.info("#{removed_aoms |> Enum.count()} removed AOMs. reseau_id codes: #{Enum.join(removed_aoms, ", ")}")
360+
Logger.info("#{new_aoms |> Enum.count()} new AOMs. SIRENs: #{Enum.join(new_aoms, ", ")}")
361+
Logger.info("#{removed_aoms |> Enum.count()} removed AOMs. SIRENs: #{Enum.join(removed_aoms, ", ")}")
354362

355363
# Some Ecto fun: two ways of joining through assoc, see https://hexdocs.pm/ecto/associations.html
356364
deleted_aom_datasets =
357365
DB.Dataset
358366
|> join(:left, [d], aom in assoc(d, :aom))
359-
|> where([d, aom], aom.composition_res_id in ^(removed_aoms |> MapSet.to_list()))
360-
|> select([d, aom], [aom.id, aom.composition_res_id, d.id])
367+
|> where([d, aom], aom.siren in ^(removed_aoms |> MapSet.to_list()))
368+
|> select([d, aom], [aom.id, aom.siren, d.id])
361369
|> DB.Repo.all()
362370
|> Enum.group_by(&hd(&1))
363371

364372
Logger.info(
365-
"Datasets still associated with deleted AOM as territory (aom.id => [aom.id, aom.composition_res_id, dataset.id]) : #{inspect(deleted_aom_datasets)}"
373+
"Datasets still associated with deleted AOM as territory (aom.id => [aom.id, aom.siren, dataset.id]) : #{inspect(deleted_aom_datasets)}"
366374
)
367375

368376
deleted_legal_owners_query =
369377
from(d in DB.Dataset,
370378
# This magically works with the many_to_many
371379
join: aom in assoc(d, :legal_owners_aom),
372-
where: aom.composition_res_id in ^(removed_aoms |> MapSet.to_list()),
373-
select: [aom.id, aom.composition_res_id, d.id]
380+
where: aom.siren in ^(removed_aoms |> MapSet.to_list()),
381+
select: [aom.id, aom.siren, d.id]
374382
)
375383

376384
deleted_legal_owners = deleted_legal_owners_query |> DB.Repo.all() |> Enum.group_by(&hd(&1))
377385

378386
Logger.info(
379-
"Datasets still associated with deleted AOM as legal owner (aom.id => [aom.id, aom.composition_res_id, dataset.id]): #{inspect(deleted_legal_owners)}"
387+
"Datasets still associated with deleted AOM as legal owner (aom.id => [aom.id, aom.siren, dataset.id]): #{inspect(deleted_legal_owners)}"
380388
)
381389
end
382390
end

0 commit comments

Comments
 (0)