decoupage_administratif 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +55 -0
- data/CHANGELOG.md +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +320 -0
- data/Rakefile +14 -0
- data/data/communes.json +37640 -0
- data/data/departements.json +111 -0
- data/data/epci.json +1257 -0
- data/data/regions.json +28 -0
- data/lib/decoupage_administratif/base_model.rb +60 -0
- data/lib/decoupage_administratif/commune.rb +82 -0
- data/lib/decoupage_administratif/config.rb +47 -0
- data/lib/decoupage_administratif/departement.rb +51 -0
- data/lib/decoupage_administratif/epci.rb +61 -0
- data/lib/decoupage_administratif/parser.rb +41 -0
- data/lib/decoupage_administratif/railtie.rb +11 -0
- data/lib/decoupage_administratif/region.rb +50 -0
- data/lib/decoupage_administratif/search.rb +181 -0
- data/lib/decoupage_administratif/territory_extensions.rb +35 -0
- data/lib/decoupage_administratif/territory_strategies.rb +87 -0
- data/lib/decoupage_administratif/version.rb +7 -0
- data/lib/decoupage_administratif.rb +26 -0
- data/lib/tasks/install.rake +59 -0
- data/sig/decoupage_administratif/base_model.rbs +7 -0
- data/sig/decoupage_administratif/commune.rbs +32 -0
- data/sig/decoupage_administratif/departement.rbs +24 -0
- data/sig/decoupage_administratif/epci.rbs +23 -0
- data/sig/decoupage_administratif/parser.rbs +11 -0
- data/sig/decoupage_administratif/region.rbs +21 -0
- data/sig/decoupage_administratif/search.rbs +20 -0
- data/sig/decoupage_administratif/territory_extensions.rbs +11 -0
- data/sig/decoupage_administratif/territory_strategies.rbs +51 -0
- data/sig/decoupage_administratif.rbs +4 -0
- metadata +96 -0
data/data/regions.json
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
[
|
2
|
+
{"code":"01","chefLieu":"97105","nom":"Guadeloupe","typeLiaison":3,"zone":"drom"},
|
3
|
+
{"code":"02","chefLieu":"97209","nom":"Martinique","typeLiaison":3,"zone":"drom"},
|
4
|
+
{"code":"03","chefLieu":"97302","nom":"Guyane","typeLiaison":3,"zone":"drom"},
|
5
|
+
{"code":"04","chefLieu":"97411","nom":"La Réunion","typeLiaison":0,"zone":"drom"},
|
6
|
+
{"code":"06","chefLieu":"97611","nom":"Mayotte","typeLiaison":0,"zone":"drom"},
|
7
|
+
{"code":"11","chefLieu":"75056","nom":"Île-de-France","typeLiaison":1,"zone":"metro"},
|
8
|
+
{"code":"24","chefLieu":"45234","nom":"Centre-Val de Loire","typeLiaison":2,"zone":"metro"},
|
9
|
+
{"code":"27","chefLieu":"21231","nom":"Bourgogne-Franche-Comté","typeLiaison":0,"zone":"metro"},
|
10
|
+
{"code":"28","chefLieu":"76540","nom":"Normandie","typeLiaison":0,"zone":"metro"},
|
11
|
+
{"code":"32","chefLieu":"59350","nom":"Hauts-de-France","typeLiaison":4,"zone":"metro"},
|
12
|
+
{"code":"44","chefLieu":"67482","nom":"Grand Est","typeLiaison":2,"zone":"metro"},
|
13
|
+
{"code":"52","chefLieu":"44109","nom":"Pays de la Loire","typeLiaison":4,"zone":"metro"},
|
14
|
+
{"code":"53","chefLieu":"35238","nom":"Bretagne","typeLiaison":0,"zone":"metro"},
|
15
|
+
{"code":"75","chefLieu":"33063","nom":"Nouvelle-Aquitaine","typeLiaison":3,"zone":"metro"},
|
16
|
+
{"code":"76","chefLieu":"31555","nom":"Occitanie","typeLiaison":1,"zone":"metro"},
|
17
|
+
{"code":"84","chefLieu":"69123","nom":"Auvergne-Rhône-Alpes","typeLiaison":1,"zone":"metro"},
|
18
|
+
{"code":"93","chefLieu":"13055","nom":"Provence-Alpes-Côte d'Azur","typeLiaison":0,"zone":"metro"},
|
19
|
+
{"code":"94","chefLieu":"2A004","nom":"Corse","typeLiaison":0,"zone":"metro"},
|
20
|
+
{"code":"975","chefLieu":"97502","nom":"Saint-Pierre-et-Miquelon","typeLiaison":0,"zone":"com"},
|
21
|
+
{"code":"977","chefLieu":"97701","nom":"Saint-Barthélemy","typeLiaison":0,"zone":"com"},
|
22
|
+
{"code":"978","chefLieu":"97801","nom":"Saint-Martin","typeLiaison":0,"zone":"com"},
|
23
|
+
{"code":"984","chefLieu":"97502","nom":"Terres australes et antarctiques françaises","typeLiaison":0,"zone":"com"},
|
24
|
+
{"code":"986","chefLieu":"98613","nom":"Wallis et Futuna","typeLiaison":0,"zone":"com"},
|
25
|
+
{"code":"987","chefLieu":"98735","nom":"Polynésie française","typeLiaison":0,"zone":"com"},
|
26
|
+
{"code":"988","chefLieu":"98818","nom":"Nouvelle-Calédonie","typeLiaison":0,"zone":"com"},
|
27
|
+
{"code":"989","chefLieu":"98901","nom":"Île de Clipperton","typeLiaison":0,"zone":"com"}
|
28
|
+
]
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
module BaseModel
|
5
|
+
# Find a single record by its code (primary key equivalent)
|
6
|
+
# @param code [String] the code to search for
|
7
|
+
# @return [Object, nil] the element with the given code, or nil if not found
|
8
|
+
# @raise [NotFoundError] if the record is not found and no default block is given
|
9
|
+
# @example
|
10
|
+
# DecoupageAdministratif::Commune.find('72039')
|
11
|
+
# DecoupageAdministratif::Region.find('52')
|
12
|
+
def find(code)
|
13
|
+
result = find_by(code: code)
|
14
|
+
raise DecoupageAdministratif::NotFoundError, "#{name.split('::').last} not found for code #{code}" if result.nil?
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param criteria [Hash] a hash with the attributes to filter by
|
20
|
+
# @return [untyped] the element that matches the criteria
|
21
|
+
# @example
|
22
|
+
# DecoupageAdministratif::Commune.find_by(nom: 'Paris')
|
23
|
+
def find_by(criteria)
|
24
|
+
all.find { |item| criteria.all? { |k, v| item.send(k) == v } }
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param args [Hash] keyword arguments containing filter criteria and options
|
28
|
+
# @option args [Boolean] :case_insensitive perform case-insensitive matching
|
29
|
+
# @option args [Boolean] :partial allow partial string matching (contains)
|
30
|
+
# @return [Array] an array of all items that match the criteria
|
31
|
+
# @example
|
32
|
+
# DecoupageAdministratif::Departement.where(code_region: '52')
|
33
|
+
# DecoupageAdministratif::Commune.where(nom: 'pari', case_insensitive: true, partial: true)
|
34
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
35
|
+
def where(**args)
|
36
|
+
# Separate options from criteria
|
37
|
+
case_insensitive = args.delete(:case_insensitive) || false
|
38
|
+
partial = args.delete(:partial) || false
|
39
|
+
|
40
|
+
all.select do |item|
|
41
|
+
args.all? do |key, value|
|
42
|
+
item_value = item.send(key)
|
43
|
+
|
44
|
+
if case_insensitive && value.is_a?(String) && item_value.is_a?(String)
|
45
|
+
if partial
|
46
|
+
item_value.downcase.include?(value.downcase)
|
47
|
+
else
|
48
|
+
item_value.downcase == value.downcase
|
49
|
+
end
|
50
|
+
elsif partial && value.is_a?(String) && item_value.is_a?(String)
|
51
|
+
item_value.include?(value)
|
52
|
+
else
|
53
|
+
item_value == value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
class Commune
|
5
|
+
extend BaseModel
|
6
|
+
include TerritoryExtensions
|
7
|
+
|
8
|
+
# @!attribute [r] code
|
9
|
+
# @return [String] INSEE code of the commune
|
10
|
+
# @!attribute [r] nom
|
11
|
+
# @return [String] Name of the commune
|
12
|
+
# @!attribute [r] zone
|
13
|
+
# @return [String] Zone of the commune ("metro", "drom", "com")
|
14
|
+
# @!attribute [r] region_code
|
15
|
+
# @return [String] INSEE code of the region
|
16
|
+
# @!attribute [r] departement_code
|
17
|
+
# @return [String] INSEE code of the department
|
18
|
+
# @!attribute [r] commune_type
|
19
|
+
# @return [Symbol] Type of the commune. Possible values:
|
20
|
+
# - :commune_actuelle: Standard current commune
|
21
|
+
# - :commune_deleguee: Delegated commune
|
22
|
+
# - :commune_associee: Associated commune
|
23
|
+
# @note Default value is :commune_actuelle
|
24
|
+
attr_reader :code, :nom, :zone, :region_code, :departement_code, :commune_type
|
25
|
+
|
26
|
+
# rubocop:disable Metrics/ParameterLists
|
27
|
+
# @param code [String] the INSEE code of the commune
|
28
|
+
# @param nom [String] the name of the commune
|
29
|
+
# @param zone [String] the zone of the commune ("metro", "drom", "com")
|
30
|
+
# @param region_code [String] the INSEE code of the region
|
31
|
+
# @param departement_code [String] the INSEE code of the department
|
32
|
+
# @param commune_type [Symbol] the type of the commune (default: :commune_actuelle)
|
33
|
+
def initialize(code:, nom:, zone:, region_code:, departement_code:, commune_type: :commune_actuelle)
|
34
|
+
@code = code
|
35
|
+
@nom = nom
|
36
|
+
@zone = zone
|
37
|
+
@region_code = region_code
|
38
|
+
@departement_code = departement_code
|
39
|
+
@commune_type = commune_type
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/ParameterLists
|
42
|
+
|
43
|
+
# @return [Array<Commune>] a collection of all communes
|
44
|
+
def self.all
|
45
|
+
@all ||= Parser.new('communes').data.map do |commune_data|
|
46
|
+
Commune.new(
|
47
|
+
code: commune_data["code"],
|
48
|
+
nom: commune_data["nom"],
|
49
|
+
zone: commune_data["zone"],
|
50
|
+
region_code: commune_data["region"],
|
51
|
+
departement_code: commune_data["departement"],
|
52
|
+
commune_type: commune_data["type"]&.gsub("-", "_")&.to_sym
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array<Commune>] a collection of all communes _actuelles_
|
58
|
+
def self.actuelles
|
59
|
+
@actuelles ||= where(commune_type: :commune_actuelle)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @raise [NotFoundError] if no region is found for the code
|
63
|
+
# @return [Region] the region of the commune
|
64
|
+
def region
|
65
|
+
@region ||= DecoupageAdministratif::Region.find(@region_code)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @raise [NotFoundError] if no department is found for the code
|
69
|
+
# @return [Departement] the department of the commune
|
70
|
+
def departement
|
71
|
+
@departement ||= DecoupageAdministratif::Departement.find(@departement_code)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Epci, nil] the EPCI of the commune, if it belongs to one
|
75
|
+
def epci
|
76
|
+
found_epci = DecoupageAdministratif::Epci.all.find do |epci|
|
77
|
+
epci.membres.any? { |m| m["code"] == @code }
|
78
|
+
end
|
79
|
+
found_epci.is_a?(DecoupageAdministratif::Epci) ? (@epci ||= found_epci) : nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
class Config
|
5
|
+
class << self
|
6
|
+
# @return [String] the directory where data files are stored
|
7
|
+
def data_directory
|
8
|
+
@data_directory ||= determine_data_directory
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param path [String] the path to set as data directory
|
12
|
+
attr_writer :data_directory
|
13
|
+
|
14
|
+
# @return [String] the directory where embedded data files are stored
|
15
|
+
def embedded_data_directory
|
16
|
+
@embedded_data_directory ||= File.join(gem_root, 'data')
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [String] the root directory of the gem
|
20
|
+
def gem_root
|
21
|
+
@gem_root ||= File.expand_path('../..', __dir__)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def determine_data_directory
|
27
|
+
ENV['DECOUPAGE_DATA_DIR'] ||
|
28
|
+
user_data_directory ||
|
29
|
+
fallback_directory
|
30
|
+
end
|
31
|
+
|
32
|
+
def user_data_directory
|
33
|
+
return nil unless Dir.home
|
34
|
+
|
35
|
+
File.join(Dir.home, '.local', 'share', 'decoupage_administratif')
|
36
|
+
end
|
37
|
+
|
38
|
+
def fallback_directory
|
39
|
+
if defined?(Rails) && Rails.respond_to?(:root)
|
40
|
+
Rails.root.join('tmp', 'decoupage_administratif').to_s
|
41
|
+
else
|
42
|
+
File.join(Dir.tmpdir, 'decoupage_administratif')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
class Departement
|
5
|
+
extend BaseModel
|
6
|
+
include TerritoryExtensions
|
7
|
+
# @!attribute [r] code
|
8
|
+
# @return [String] INSEE code of the department
|
9
|
+
# @!attribute [r] nom
|
10
|
+
# @return [String] Name of the department
|
11
|
+
# @!attribute [r] zone
|
12
|
+
# @return [String] Zone of the department ("metro", "drom", "com")
|
13
|
+
# @!attribute [r] code_region
|
14
|
+
# @return [String] INSEE code of the region
|
15
|
+
attr_reader :code, :nom, :zone, :code_region
|
16
|
+
|
17
|
+
# @param code [String] the INSEE code of the department
|
18
|
+
# @param nom [String] the name of the department
|
19
|
+
# @param zone [String] the zone of the department ("metro", "drom", "com")
|
20
|
+
# @param code_region [String] the INSEE code of the region
|
21
|
+
def initialize(code:, nom:, zone:, code_region:)
|
22
|
+
@code = code
|
23
|
+
@nom = nom
|
24
|
+
@zone = zone
|
25
|
+
@code_region = code_region
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Array<Departement>] a collection of all departments
|
29
|
+
def self.all
|
30
|
+
@all ||= Parser.new('departements').data.map do |departement_data|
|
31
|
+
DecoupageAdministratif::Departement.new(
|
32
|
+
code: departement_data["code"],
|
33
|
+
nom: departement_data["nom"],
|
34
|
+
zone: departement_data["zone"],
|
35
|
+
code_region: departement_data["region"]
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Array<Commune>] a collection of all actual communes in the department
|
41
|
+
def communes
|
42
|
+
@communes ||= DecoupageAdministratif::Commune.where(departement_code: @code, commune_type: :commune_actuelle)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @raise [NotFoundError] if no region is found for the code
|
46
|
+
# @return [Region] the region of the department
|
47
|
+
def region
|
48
|
+
@region ||= DecoupageAdministratif::Region.find(@code_region)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
class Epci
|
5
|
+
extend BaseModel
|
6
|
+
include TerritoryExtensions
|
7
|
+
|
8
|
+
# @!attribute [r] code
|
9
|
+
# @return [String] SIREN code of the EPCI
|
10
|
+
# @!attribute [r] nom
|
11
|
+
# @return [String] Name of the EPCI
|
12
|
+
# @!attribute [r] membres
|
13
|
+
# @return [Array<Hash>] Members of the EPCI, each member is a hash with "nom" and "code" keys
|
14
|
+
attr_reader :code, :nom, :membres
|
15
|
+
|
16
|
+
# @param code [String] the SIREN code of the EPCI
|
17
|
+
# @param nom [String] the name of the EPCI
|
18
|
+
# @param membres [Array<Hash>] the members of the EPCI, each member is a hash with "nom" and "code" keys
|
19
|
+
def initialize(code:, nom:, membres: [])
|
20
|
+
@code = code
|
21
|
+
@nom = nom
|
22
|
+
@membres = membres
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Array<Epci>] a collection of all EPCI
|
26
|
+
def self.all
|
27
|
+
Parser.new('epci').data.map do |epci_data|
|
28
|
+
Epci.new(
|
29
|
+
code: epci_data["code"],
|
30
|
+
nom: epci_data["nom"],
|
31
|
+
membres: epci_data["membres"].map { |membre| membre.slice("nom", "code") }
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Search for an EPCI that includes all the specified codes
|
37
|
+
# @param codes [Array<String>] an array of commune codes
|
38
|
+
# @return [Array<Epci>] a collection of EPCI that include all the specified codes
|
39
|
+
def self.search_by_communes_codes(codes)
|
40
|
+
all.select do |epci|
|
41
|
+
epci.membres.map do |m|
|
42
|
+
codes.include?(m['code'])
|
43
|
+
end.all?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Array<Commune>] a collection of all communes that are members of the EPCI
|
48
|
+
# @raise [NotFoundError] if a commune code is not found
|
49
|
+
def communes
|
50
|
+
@communes ||= @membres.map do |membre|
|
51
|
+
DecoupageAdministratif::Commune.find(membre["code"])
|
52
|
+
end.compact
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Array<Region>] an array of regions that the EPCI communes belong to
|
56
|
+
# Sometimes an EPCI can have communes from different regions.
|
57
|
+
def regions
|
58
|
+
@regions ||= communes.map(&:region).uniq
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module DecoupageAdministratif
|
6
|
+
class Parser
|
7
|
+
# @!attribute [r] data
|
8
|
+
# @return [Array<Hash>] Parsed data from the JSON file for the given model
|
9
|
+
attr_reader :data
|
10
|
+
|
11
|
+
# @param model [String] the name of the model to parse (e.g., 'communes', 'departements', 'regions')
|
12
|
+
# @return [Parser] a new Parser instance
|
13
|
+
# @note Only expected model names should be used to avoid loading unwanted files. The file path is constructed from the model name.
|
14
|
+
def initialize(model)
|
15
|
+
@model = model
|
16
|
+
@file_path = File.join(DecoupageAdministratif::Config.data_directory, "#{@model}.json")
|
17
|
+
load_data
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def load_data
|
23
|
+
file = File.read(@file_path)
|
24
|
+
@data = JSON.parse(file)
|
25
|
+
rescue Errno::ENOENT
|
26
|
+
# Try to load from embedded data if external file not found
|
27
|
+
@file_path = File.join(DecoupageAdministratif::Config.embedded_data_directory, "#{@model}.json")
|
28
|
+
begin
|
29
|
+
file = File.read(@file_path)
|
30
|
+
@data = JSON.parse(file)
|
31
|
+
rescue Errno::ENOENT
|
32
|
+
raise Error,
|
33
|
+
"File #{@file_path} does not exist. You can update the data with 'rake decoupage_administratif:update'"
|
34
|
+
rescue JSON::ParserError
|
35
|
+
raise Error, "File #{@model}.json is not valid JSON"
|
36
|
+
end
|
37
|
+
rescue JSON::ParserError
|
38
|
+
raise Error, "File #{@model}.json is not valid JSON"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
class Region
|
5
|
+
extend BaseModel
|
6
|
+
include TerritoryExtensions
|
7
|
+
|
8
|
+
# @!attribute [r] code
|
9
|
+
# @return [String] INSEE code of the region
|
10
|
+
# @!attribute [r] nom
|
11
|
+
# @return [String] Name of the region
|
12
|
+
# @!attribute [r] zone
|
13
|
+
# @return [String] Zone of the region ("metro", "drom", "com")
|
14
|
+
attr_reader :code, :nom, :zone
|
15
|
+
|
16
|
+
# @param code [String] the INSEE code of the region
|
17
|
+
# @param nom [String] the name of the region
|
18
|
+
# @param zone [String] the zone of the region ("metro", "drom", "com")
|
19
|
+
def initialize(code:, nom:, zone:)
|
20
|
+
@code = code
|
21
|
+
@nom = nom
|
22
|
+
@zone = zone
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Array<Region>] a collection of all regions
|
26
|
+
def self.all
|
27
|
+
@all ||= Parser.new('regions').data.map do |region_data|
|
28
|
+
Region.new(
|
29
|
+
code: region_data["code"],
|
30
|
+
nom: region_data["nom"],
|
31
|
+
zone: region_data["zone"]
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Array<Departement>] a collection of all departments in the region
|
37
|
+
def departements
|
38
|
+
@departements ||= DecoupageAdministratif::Departement.all.select do |departement|
|
39
|
+
departement.code_region == @code
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Array<Commune>] a collection of all actual communes in the region
|
44
|
+
def communes
|
45
|
+
@communes ||= DecoupageAdministratif::Commune.all.select do |commune|
|
46
|
+
commune.region_code == @code && commune.commune_type == :commune_actuelle
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
class Search
|
5
|
+
# @!attribute [r] codes
|
6
|
+
# @return [Array<String>, nil] List of INSEE codes used for the search
|
7
|
+
# @!attribute [r] regions
|
8
|
+
# @return [Array<Region>] Regions found by the search
|
9
|
+
# @!attribute [r] departements
|
10
|
+
# @return [Array<Departement>] Departments found by the search
|
11
|
+
# @!attribute [r] epcis
|
12
|
+
# @return [Array<Epci>] EPCIs found by the search
|
13
|
+
# @!attribute [r] communes
|
14
|
+
# @return [Array<Commune>] Communes found by the search
|
15
|
+
attr_reader :codes, :regions, :departements, :epcis, :communes
|
16
|
+
|
17
|
+
def initialize(codes = nil)
|
18
|
+
@codes = codes&.uniq
|
19
|
+
initialize_class_caches
|
20
|
+
end
|
21
|
+
|
22
|
+
# Search for territories by municipality.
|
23
|
+
# If the list of municipalities represents a department or an EPCI, the corresponding territories are displayed.
|
24
|
+
# If the codes do not correspond to any territory, a list of municipalities is returned.
|
25
|
+
# @return [Hash] a hash containing the regions, departments, EPCI, and communes found
|
26
|
+
# @note Returns empty arrays if no territories are found for the given codes.
|
27
|
+
def by_insee_codes
|
28
|
+
@codes = group_by_departement
|
29
|
+
@codes = find_communes_by_codes
|
30
|
+
|
31
|
+
search_for_departements
|
32
|
+
search_for_region
|
33
|
+
search_for_epcis
|
34
|
+
search_for_communes
|
35
|
+
|
36
|
+
{
|
37
|
+
regions: @regions,
|
38
|
+
departements: @departements,
|
39
|
+
epcis: @epcis,
|
40
|
+
communes: @communes
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return the territories associated with a given INSEE code.
|
45
|
+
# @param code_insee [String] the INSEE code of the commune
|
46
|
+
# @return [Hash] a hash containing the EPCI, department, and region associated with the commune
|
47
|
+
def find_territories_by_commune_insee_code(code_insee)
|
48
|
+
# Use cache if available, fallback to find_by for compatibility with tests
|
49
|
+
commune = @@communes_cache&.[](code_insee) || DecoupageAdministratif::Commune.find_by(code: code_insee)
|
50
|
+
return {} if commune.nil?
|
51
|
+
|
52
|
+
{
|
53
|
+
epci: commune.epci,
|
54
|
+
departement: commune.departement,
|
55
|
+
region: commune.region
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Class-level caches for performance optimization
|
62
|
+
# rubocop:disable Style/ClassVars
|
63
|
+
@@departements_cache = nil
|
64
|
+
@@communes_cache = nil
|
65
|
+
# rubocop:enable Style/ClassVars
|
66
|
+
|
67
|
+
# Initialize class-level caches for performance optimization
|
68
|
+
# @return [void]
|
69
|
+
def initialize_class_caches
|
70
|
+
return if @@departements_cache && @@communes_cache
|
71
|
+
|
72
|
+
# rubocop:disable Style/ClassVars
|
73
|
+
@@departements_cache = DecoupageAdministratif::Departement.all.each_with_object({}) { |dept, hash| hash[dept.code] = dept }
|
74
|
+
@@communes_cache = DecoupageAdministratif::Commune.actuelles.each_with_object({}) { |commune, hash| hash[commune.code] = commune }
|
75
|
+
# rubocop:enable Style/ClassVars
|
76
|
+
end
|
77
|
+
|
78
|
+
# Group the codes by department.
|
79
|
+
# @return [Hash<Departement, Array<Commune>>] Hash with departments as keys and their communes as values
|
80
|
+
def group_by_departement
|
81
|
+
# Group the codes by DecoupageAdministratif::Departement
|
82
|
+
# and DecoupageAdministratif::Communes as values
|
83
|
+
@codes.group_by do |code|
|
84
|
+
dept_code = code[0..1] == "97" ? code[0..2] : code[0..1]
|
85
|
+
@@departements_cache[dept_code]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Find communes by their codes.
|
90
|
+
# @return [Array<Commune>] List of communes matching the codes
|
91
|
+
def find_communes_by_codes
|
92
|
+
@codes.transform_values do |codes_insee|
|
93
|
+
codes_insee.filter_map do |code|
|
94
|
+
commune = @@communes_cache[code]
|
95
|
+
next if commune.nil?
|
96
|
+
|
97
|
+
commune.commune_type == :commune_actuelle ? commune : nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Search for departments matching the codes.
|
103
|
+
# @return [void]
|
104
|
+
def search_for_departements
|
105
|
+
@departements = []
|
106
|
+
return if only_nil_key?
|
107
|
+
|
108
|
+
departements_to_delete = []
|
109
|
+
@codes.each do |departement, communes|
|
110
|
+
next unless should_add_departement?(departement, communes)
|
111
|
+
|
112
|
+
@departements << departement
|
113
|
+
departements_to_delete << departement
|
114
|
+
end
|
115
|
+
departements_to_delete.each { |dep_code| @codes.delete(dep_code) }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Check if only one key and it is nil
|
119
|
+
# @return [Boolean]
|
120
|
+
def only_nil_key?
|
121
|
+
@codes.keys.uniq.count == 1 && @codes.keys.first.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
# Should add departement to result?
|
125
|
+
# @param departement [Departement, nil]
|
126
|
+
# @param communes [Array<Commune>]
|
127
|
+
# @return [Boolean]
|
128
|
+
def should_add_departement?(departement, communes)
|
129
|
+
return false if departement.nil?
|
130
|
+
|
131
|
+
departement.communes.count == communes.count && departement.communes.map(&:code).sort == communes.map(&:code).sort
|
132
|
+
end
|
133
|
+
|
134
|
+
# Search for regions matching the codes.
|
135
|
+
# @return [void]
|
136
|
+
def search_for_region
|
137
|
+
@regions = []
|
138
|
+
return if @departements.empty?
|
139
|
+
|
140
|
+
regions = DecoupageAdministratif::Region.all
|
141
|
+
regions.each do |region|
|
142
|
+
next unless region.departements.all? do |departement|
|
143
|
+
@departements.map(&:code).include? departement.code
|
144
|
+
end
|
145
|
+
|
146
|
+
@regions << region
|
147
|
+
region.departements.each do |departement|
|
148
|
+
@departements.delete(departement)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Search for EPCIs matching the codes.
|
154
|
+
# @return [void]
|
155
|
+
def search_for_epcis
|
156
|
+
@epcis = []
|
157
|
+
return if @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
|
158
|
+
|
159
|
+
@codes.each_value do |communes|
|
160
|
+
# Find EPCIs that match all communes in the current group
|
161
|
+
@epcis = DecoupageAdministratif::Epci.search_by_communes_codes(communes.map(&:code))
|
162
|
+
@epcis.each do |epci|
|
163
|
+
communes.reject! do |commune|
|
164
|
+
epci.communes.include?(commune)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Search for communes matching the codes.
|
171
|
+
# @return [void]
|
172
|
+
def search_for_communes
|
173
|
+
@communes = []
|
174
|
+
return if @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
|
175
|
+
|
176
|
+
@codes.each_value do |communes|
|
177
|
+
communes.map { |commune| @communes << commune }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DecoupageAdministratif
|
4
|
+
module TerritoryExtensions
|
5
|
+
# Check if this territory intersects with a list of commune INSEE codes
|
6
|
+
# @param commune_insee_codes [Array<String>] array of commune INSEE codes to check against
|
7
|
+
# @return [Boolean] true if territory intersects with any of the provided codes
|
8
|
+
def territory_intersects_with_insee_codes?(commune_insee_codes)
|
9
|
+
territory_strategy.intersects_with_insee_codes?(commune_insee_codes)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get commune INSEE codes for this territory
|
13
|
+
# @return [Array<String>] array of commune INSEE codes covered by this territory
|
14
|
+
def territory_insee_codes
|
15
|
+
@territory_insee_codes ||= territory_strategy.insee_codes
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def territory_strategy
|
21
|
+
@territory_strategy ||= case self.class.name.split('::').last
|
22
|
+
when 'Commune'
|
23
|
+
TerritoryStrategies::CommuneStrategy.new(self)
|
24
|
+
when 'Departement'
|
25
|
+
TerritoryStrategies::DepartementStrategy.new(self)
|
26
|
+
when 'Region'
|
27
|
+
TerritoryStrategies::RegionStrategy.new(self)
|
28
|
+
when 'Epci'
|
29
|
+
TerritoryStrategies::EpciStrategy.new(self)
|
30
|
+
else
|
31
|
+
raise NotImplementedError, "Territory strategy not implemented for #{self.class.name}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|