us_geo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +5 -0
  3. data/Gemfile.lock +75 -0
  4. data/README.md +154 -0
  5. data/Rakefile +18 -0
  6. data/db/migrate/20190221054200_create_regions.rb +16 -0
  7. data/db/migrate/20190221054300_create_divisions.rb +17 -0
  8. data/db/migrate/20190221054400_create_states.rb +20 -0
  9. data/db/migrate/20190221054490_create_designated_market_areas.rb +16 -0
  10. data/db/migrate/20190221054500_create_combined_statistical_areas.rb +20 -0
  11. data/db/migrate/20190221054600_create_core_based_statistical_areas.rb +24 -0
  12. data/db/migrate/20190221054650_create_metropolitan_divisions.rb +21 -0
  13. data/db/migrate/20190221054700_create_counties.rb +34 -0
  14. data/db/migrate/20190221054800_create_zctas.rb +23 -0
  15. data/db/migrate/20190221054900_create_zcta_counties.rb +22 -0
  16. data/db/migrate/20190221055000_create_urban_areas.rb +25 -0
  17. data/db/migrate/20190221055100_create_urban_area_counties.rb +22 -0
  18. data/db/migrate/20190221055200_create_zcta_urban_areas.rb +22 -0
  19. data/db/migrate/20190221060000_create_places.rb +28 -0
  20. data/db/migrate/20190221061000_create_place_counties.rb +18 -0
  21. data/db/migrate/20190221062000_create_zcta_places.rb +22 -0
  22. data/db/migrate/20190221063000_create_county_subdivisions.rb +25 -0
  23. data/lib/tasks/us_geo/us_geo.rake +43 -0
  24. data/lib/us_geo/base_record.rb +104 -0
  25. data/lib/us_geo/combined_statistical_area.rb +40 -0
  26. data/lib/us_geo/core_based_statistical_area.rb +57 -0
  27. data/lib/us_geo/county.rb +89 -0
  28. data/lib/us_geo/county_subdivision.rb +46 -0
  29. data/lib/us_geo/demographics.rb +25 -0
  30. data/lib/us_geo/designated_market_area.rb +30 -0
  31. data/lib/us_geo/division.rb +29 -0
  32. data/lib/us_geo/engine.rb +6 -0
  33. data/lib/us_geo/metropolitan_area.rb +18 -0
  34. data/lib/us_geo/metropolitan_division.rb +42 -0
  35. data/lib/us_geo/micropolitan_area.rb +18 -0
  36. data/lib/us_geo/place.rb +61 -0
  37. data/lib/us_geo/place_county.rb +28 -0
  38. data/lib/us_geo/region.rb +28 -0
  39. data/lib/us_geo/state.rb +56 -0
  40. data/lib/us_geo/urban_area.rb +66 -0
  41. data/lib/us_geo/urban_area_county.rb +68 -0
  42. data/lib/us_geo/urban_cluster.rb +18 -0
  43. data/lib/us_geo/urbanized_area.rb +18 -0
  44. data/lib/us_geo/version.rb +5 -0
  45. data/lib/us_geo/zcta.rb +63 -0
  46. data/lib/us_geo/zcta_county.rb +68 -0
  47. data/lib/us_geo/zcta_place.rb +68 -0
  48. data/lib/us_geo/zcta_urban_area.rb +68 -0
  49. data/lib/us_geo.rb +53 -0
  50. data/spec/spec_helper.rb +22 -0
  51. data/spec/us_geo/base_record_spec.rb +67 -0
  52. data/spec/us_geo/combined_statistical_area_spec.rb +33 -0
  53. data/spec/us_geo/core_based_statistical_area_spec.rb +56 -0
  54. data/spec/us_geo/county_spec.rb +130 -0
  55. data/spec/us_geo/county_subdivision_spec.rb +37 -0
  56. data/spec/us_geo/demographics_spec.rb +19 -0
  57. data/spec/us_geo/designated_market_area_spec.rb +29 -0
  58. data/spec/us_geo/division_spec.rb +37 -0
  59. data/spec/us_geo/metropolitan_division_spec.rb +41 -0
  60. data/spec/us_geo/place_county_spec.rb +39 -0
  61. data/spec/us_geo/place_spec.rb +71 -0
  62. data/spec/us_geo/region_spec.rb +36 -0
  63. data/spec/us_geo/state_spec.rb +70 -0
  64. data/spec/us_geo/urban_area_county_spec.rb +82 -0
  65. data/spec/us_geo/urban_area_spec.rb +98 -0
  66. data/spec/us_geo/zcta_county_spec.rb +82 -0
  67. data/spec/us_geo/zcta_place_spec.rb +82 -0
  68. data/spec/us_geo/zcta_spec.rb +99 -0
  69. data/spec/us_geo/zcta_urban_area_spec.rb +82 -0
  70. metadata +229 -0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :us_geo do
4
+ namespace :import do
5
+ klasses = {
6
+ regions: USGeo::Region,
7
+ divisions: USGeo::Division,
8
+ states: USGeo::State,
9
+ designated_market_areas: USGeo::DesignatedMarketArea,
10
+ combined_statistical_areas: USGeo::CombinedStatisticalArea,
11
+ core_based_statistical_areas: USGeo::CoreBasedStatisticalArea,
12
+ metropolitan_divisions: USGeo::MetropolitanDivision,
13
+ counties: USGeo::County,
14
+ county_subdivisions: USGeo::CountySubdivision,
15
+ urban_areas: USGeo::UrbanArea,
16
+ places: USGeo::Place,
17
+ zctas: USGeo::Zcta,
18
+ zcta_counties: USGeo::ZctaCounty,
19
+ zcta_urban_areas: USGeo::ZctaUrbanArea,
20
+ zcta_places: USGeo::ZctaPlace,
21
+ urban_area_counties: USGeo::UrbanAreaCounty,
22
+ place_counties: USGeo::PlaceCounty
23
+ }
24
+ klasses.each do |name, klass|
25
+ desc "Import data for #{klass}"
26
+ task name => :environment do
27
+ t = Time.now
28
+ klass.load!
29
+ puts "Loaded #{klass.count} rows into #{klass.table_name} in #{(Time.now - t).round(1)}s"
30
+ klass.removed.find_each do |record|
31
+ puts(" WARNING: #{klass}.#{record.id} status changed to removed")
32
+ end
33
+ end
34
+
35
+ desc "Import data for all USGeo models"
36
+ task all: :environment do
37
+ klasses.each_key do |name|
38
+ Rake::Task["us_geo:import:#{name}"].invoke
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "open-uri"
5
+
6
+ module USGeo
7
+
8
+ class LoadError < StandardError
9
+ end
10
+
11
+ # Base class that all models inherit from.
12
+ class BaseRecord < ::ActiveRecord::Base
13
+
14
+ self.abstract_class = true
15
+ self.table_name_prefix = "us_geo_"
16
+
17
+ STATUS_IMPORTED = 1
18
+ STATUS_REMOVED = -1
19
+ STATUS_MANUAL = 0
20
+
21
+ validates :status, inclusion: [STATUS_IMPORTED, STATUS_REMOVED, STATUS_MANUAL]
22
+
23
+ scope :imported, -> { where(status: STATUS_IMPORTED) }
24
+ scope :removed, -> { where(status: STATUS_REMOVED) }
25
+ scope :manual, -> { where(status: STATUS_MANUAL) }
26
+ scope :not_removed, -> { where(status: [STATUS_IMPORTED, STATUS_MANUAL]) }
27
+
28
+ class << self
29
+ def load!(location = nil, gzipped: true)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ protected
34
+
35
+ # Insert or update a record given the unique criteria for finding it.
36
+ def load_record!(criteria, &block)
37
+ record = find_or_initialize_by(criteria)
38
+ record.status = STATUS_IMPORTED
39
+ record.updated_at = Time.now
40
+ yield(record)
41
+ record.save!
42
+ end
43
+
44
+ # Mark the status of any records not updated in the block as being no longer imported.
45
+ def import!(&block)
46
+ start_time = Time.at(Time.now.to_i.floor)
47
+ yield
48
+ raise LoadError.new("No data found") unless where("updated_at >= ?", start_time).exists?
49
+ where("updated_at < ?", start_time).imported.update_all(status: STATUS_REMOVED)
50
+ end
51
+
52
+ def data_uri(path)
53
+ path = path.to_s if path
54
+ if path.start_with?("/") || path.include?(":")
55
+ path
56
+ elsif USGeo.base_data_uri.include?(":")
57
+ "#{USGeo.base_data_uri}/#{path}"
58
+ else
59
+ File.join(USGeo.base_data_uri, path)
60
+ end
61
+ end
62
+
63
+ def load_data_file(location, &block)
64
+ file = nil
65
+ if location.include?(":")
66
+ file = URI.parse(location).open(read_timeout: 5, open_timeout: 5)
67
+ else
68
+ file = File.open(location)
69
+ end
70
+ begin
71
+ rows = []
72
+ CSV.new(file, headers: true).each do |row|
73
+ rows << row
74
+ if rows.size >= 50
75
+ transaction { rows.each(&block) }
76
+ rows.clear
77
+ end
78
+ end
79
+ transaction { rows.each(&block) } unless rows.empty?
80
+ ensure
81
+ file.close if file && !file.closed?
82
+ end
83
+ end
84
+
85
+ # Convert square meters to square miles
86
+ def area_meters_to_miles(square_meters)
87
+ (square_meters.to_f / (1609.34 ** 2)).round(6)
88
+ end
89
+ end
90
+
91
+ def imported?
92
+ status == STATUS_IMPORTED
93
+ end
94
+
95
+ def removed?
96
+ status == STATUS_REMOVED
97
+ end
98
+
99
+ def manual?
100
+ status == STATUS_MANUAL
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Combined statistical area (CSA) of multiple metropolitan areas with weak regional
6
+ # and economic connectoins between them.
7
+ class CombinedStatisticalArea < BaseRecord
8
+
9
+ include Demographics
10
+
11
+ self.primary_key = "geoid"
12
+
13
+ has_many :core_based_statistical_areas, foreign_key: :csa_geoid, inverse_of: :combined_statistical_area
14
+
15
+ validates :geoid, length: {is: 3}
16
+ validates :name, length: {maximum: 60}
17
+ validates :land_area, numericality: true, presence: true
18
+ validates :water_area, numericality: true, presence: true
19
+ validates :population, numericality: {only_integer: true}, presence: true
20
+ validates :housing_units, numericality: {only_integer: true}, presence: true
21
+
22
+ class << self
23
+ def load!(uri = nil)
24
+ location = data_uri(uri || "combined_statistical_areas.csv")
25
+ import! do
26
+ load_data_file(location) do |row|
27
+ load_record!(geoid: row["GEOID"]) do |record|
28
+ record.name = row["Name"]
29
+ record.population = row["Population"]
30
+ record.housing_units = row["Housing Units"]
31
+ record.land_area = area_meters_to_miles(row["Land Area"])
32
+ record.water_area = area_meters_to_miles(row["Water Area"])
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Core based statistical area composed of one or more counties anchored by an urban center.
6
+ # Includes both metropolitan (population > 50,000) and micropolitan (population > 10,000
7
+ # but < 50,000) areas.
8
+ class CoreBasedStatisticalArea < BaseRecord
9
+
10
+ include Demographics
11
+
12
+ self.primary_key = "geoid"
13
+ self.store_full_sti_class = false
14
+
15
+ has_many :counties, foreign_key: :cbsa_geoid, inverse_of: :core_based_statistical_area
16
+ has_many :metropolitan_divisions, foreign_key: :cbsa_geoid, inverse_of: :core_based_statistical_area
17
+ belongs_to :combined_statistical_area, foreign_key: :csa_geoid, optional: true, inverse_of: :core_based_statistical_areas
18
+
19
+ validates :geoid, length: {is: 5}
20
+ validates :name, length: {maximum: 60}
21
+ validates :land_area, numericality: true, presence: true
22
+ validates :water_area, numericality: true, presence: true
23
+ validates :population, numericality: {only_integer: true}, presence: true
24
+ validates :housing_units, numericality: {only_integer: true}, presence: true
25
+
26
+ class << self
27
+ def load!(uri = nil)
28
+ location = data_uri(uri || "core_based_statistical_areas.csv")
29
+
30
+ import! do
31
+ load_data_file(location) do |row|
32
+ load_record!(geoid: row["GEOID"]) do |record|
33
+ record.type = (row["Population"].to_i >= 50_000 ? "MetropolitanArea" : "MicropolitanArea")
34
+ record.name = row["Name"]
35
+ record.csa_geoid = row["CSA"]
36
+ record.population = row["Population"]
37
+ record.housing_units = row["Housing Units"]
38
+ record.land_area = area_meters_to_miles(row["Land Area"])
39
+ record.water_area = area_meters_to_miles(row["Water Area"])
40
+ record.lat = row["Latitude"]
41
+ record.lng = row["Longitude"]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def metropolitan?
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def micropolitan?
53
+ raise NotImplementedError
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # County or county equivalent. Counties are composed of zero or more ZCTA's and may
6
+ # belong to a CBSA. The county's significance withing the CBSA is indicated by the
7
+ # central flag which indicates if it is a central or outlying county.
8
+ class County < BaseRecord
9
+
10
+ include Demographics
11
+
12
+ self.primary_key = "geoid"
13
+
14
+ belongs_to :designated_market_area, foreign_key: :dma_code, optional: true, inverse_of: :counties
15
+ belongs_to :core_based_statistical_area, foreign_key: :cbsa_geoid, optional: true, inverse_of: :counties
16
+ belongs_to :metropolitan_division, foreign_key: :metropolitan_division_geoid, optional: true, inverse_of: :counties
17
+ belongs_to :state, foreign_key: :state_code, inverse_of: :counties
18
+
19
+ has_many :subdivisions, foreign_key: :county_geoid, inverse_of: :county, class_name: "USGeo::CountySubdivision"
20
+
21
+ has_many :zcta_counties, foreign_key: :county_geoid, inverse_of: :county, dependent: :destroy
22
+ has_many :zctas, through: :zcta_counties
23
+
24
+ has_many :urban_area_counties, foreign_key: :county_geoid, inverse_of: :county, dependent: :destroy
25
+ has_many :urban_areas, through: :urban_area_counties
26
+
27
+ has_many :place_counties, foreign_key: :county_geoid, inverse_of: :county, dependent: :destroy
28
+ has_many :places, through: :place_counties
29
+
30
+ validates :geoid, length: {is: 5}
31
+ validates :name, length: {maximum: 60}
32
+ validates :short_name, length: {maximum: 30}
33
+ validates :state_code, length: {is: 2}
34
+ validates :fips_class_code, length: {is: 2}
35
+ validates :metropolitan_division_geoid, length: {is: 5}, allow_nil: true
36
+ validates :cbsa_geoid, length: {is: 5}, allow_nil: true
37
+ validates :dma_code, length: {is: 3}, allow_nil: true
38
+ validates :land_area, numericality: true, allow_nil: true
39
+ validates :water_area, numericality: true, allow_nil: true
40
+ validates :population, numericality: {only_integer: true}, allow_nil: true
41
+ validates :housing_units, numericality: {only_integer: true}, allow_nil: true
42
+
43
+ class << self
44
+ def load!(uri = nil)
45
+ location = data_uri(uri || "counties.csv")
46
+
47
+ import! do
48
+ load_data_file(location) do |row|
49
+ load_record!(geoid: row["GEOID"]) do |record|
50
+ record.gnis_id = row["GNIS ID"]
51
+ record.name = row["Name"]
52
+ record.short_name = row["Short Name"]
53
+ record.state_code = row["State"]
54
+ record.cbsa_geoid = row["CBSA"]
55
+ record.dma_code = row["DMA"]
56
+ record.time_zone_name = row["Time Zone"]
57
+ record.fips_class_code = row["FIPS Class"]
58
+ record.central = (row["Central"] == "T")
59
+ record.population = row["Population"]
60
+ record.housing_units = row["Housing Units"]
61
+ record.land_area = area_meters_to_miles(row["Land Area"])
62
+ record.water_area = area_meters_to_miles(row["Water Area"])
63
+ record.lat = row["Latitude"]
64
+ record.lng = row["Longitude"]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def state_fips
72
+ geoid[0, 2]
73
+ end
74
+
75
+ def county_fips
76
+ geoid[2, 3]
77
+ end
78
+
79
+ # Return the CBSA only if it is a metropolitan area.
80
+ def metropolitan_area
81
+ core_based_statistical_area if core_based_statistical_area && core_based_statistical_area.metropolitan?
82
+ end
83
+
84
+ def time_zone
85
+ ActiveSupport::TimeZone[time_zone_name] if time_zone_name
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # County subdivision.
6
+ class CountySubdivision < BaseRecord
7
+
8
+ include Demographics
9
+
10
+ self.primary_key = "geoid"
11
+
12
+ belongs_to :county, foreign_key: :county_geoid, inverse_of: :subdivisions
13
+
14
+ validates :geoid, length: {is: 10}
15
+ validates :name, length: {maximum: 60}
16
+ validates :fips_class_code, length: {is: 2}
17
+ validates :land_area, numericality: true, allow_nil: true
18
+ validates :water_area, numericality: true, allow_nil: true
19
+ validates :population, numericality: {only_integer: true}, allow_nil: true
20
+ validates :housing_units, numericality: {only_integer: true}, allow_nil: true
21
+
22
+ class << self
23
+ def load!(uri = nil)
24
+ location = data_uri(uri || "county_subdivisions.csv")
25
+
26
+ import! do
27
+ load_data_file(location) do |row|
28
+ load_record!(geoid: row["GEOID"]) do |record|
29
+ record.gnis_id = row["GNIS ID"]
30
+ record.county_geoid = row["County GEOID"]
31
+ record.name = row["Name"]
32
+ record.fips_class_code = row["FIPS Class"]
33
+ record.population = row["Population"]
34
+ record.housing_units = row["Housing Units"]
35
+ record.land_area = area_meters_to_miles(row["Land Area"])
36
+ record.water_area = area_meters_to_miles(row["Water Area"])
37
+ record.lat = row["Latitude"]
38
+ record.lng = row["Longitude"]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # This module is mixed into all models. Note that the area given for land and water
6
+ # is in square miles.
7
+ module Demographics
8
+
9
+ # Population per square mile.
10
+ def population_density
11
+ population.to_f / land_area if population && land_area
12
+ end
13
+
14
+ # Total area of both land an water in square miles
15
+ def total_area
16
+ land_area.to_f + water_area.to_f if land_area
17
+ end
18
+
19
+ # The fraction of the area that is composed of land instead of water.
20
+ def percent_land
21
+ land_area / total_area if land_area
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Media market (DMA) as defined by the Nielsen Company.
6
+ class DesignatedMarketArea < BaseRecord
7
+
8
+ self.primary_key = "code"
9
+
10
+ has_many :counties, foreign_key: :dma_code, inverse_of: :designated_market_area
11
+
12
+ validates :code, length: {is: 3}
13
+ validates :name, length: {maximum: 60}
14
+
15
+ class << self
16
+ def load!(uri = nil)
17
+ location = data_uri(uri || "dmas.csv")
18
+
19
+ import! do
20
+ load_data_file(location) do |row|
21
+ load_record!(code: row["Code"]) do |record|
22
+ record.name = row["Name"]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # U.S. regional division composed of states.
6
+ class Division < BaseRecord
7
+
8
+ belongs_to :region, inverse_of: :divisions
9
+ has_many :states, inverse_of: :division
10
+
11
+ validates :name, length: {maximum: 30}
12
+
13
+ class << self
14
+ def load!(uri = nil)
15
+ location = data_uri(uri || "divisions.csv")
16
+
17
+ import! do
18
+ load_data_file(location) do |row|
19
+ load_record!(id: row["Division ID"]) do |record|
20
+ record.name = row["Division Name"]
21
+ record.region_id = row["Region ID"]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+ class Engine < ::Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Core based statistical area with a population greater then 50,000.
6
+ class MetropolitanArea < CoreBasedStatisticalArea
7
+
8
+ def metropolitan?
9
+ true
10
+ end
11
+
12
+ def micropolitan?
13
+ false
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Division of very large metropolitian areas into groups of approximately 2.5 million people.
6
+ class MetropolitanDivision < BaseRecord
7
+
8
+ include Demographics
9
+
10
+ self.primary_key = "geoid"
11
+
12
+ has_many :counties, foreign_key: :metropolitan_division_geoid, inverse_of: :metropolitan_division
13
+ belongs_to :core_based_statistical_area, foreign_key: :cbsa_geoid, optional: true, inverse_of: :metropolitan_divisions
14
+
15
+ validates :geoid, length: {is: 5}
16
+ validates :name, length: {maximum: 60}
17
+ validates :land_area, numericality: true, presence: true
18
+ validates :water_area, numericality: true, presence: true
19
+ validates :population, numericality: {only_integer: true}, presence: true
20
+ validates :housing_units, numericality: {only_integer: true}, presence: true
21
+
22
+ class << self
23
+ def load!(uri = nil)
24
+ location = data_uri(uri || "metropolitan_divisions.csv")
25
+
26
+ import! do
27
+ load_data_file(location) do |row|
28
+ load_record!(geoid: row["GEOID"]) do |record|
29
+ record.name = row["Name"]
30
+ record.cbsa_geoid = row["CBSA"]
31
+ record.population = row["Population"]
32
+ record.housing_units = row["Housing Units"]
33
+ record.land_area = area_meters_to_miles(row["Land Area"])
34
+ record.water_area = area_meters_to_miles(row["Water Area"])
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Core based statistical area with a population greater then 10,000 but less than 50,000.
6
+ class MicropolitanArea < CoreBasedStatisticalArea
7
+
8
+ def metropolitan?
9
+ false
10
+ end
11
+
12
+ def micropolitan?
13
+ true
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Division of very large metropolitian areas into groups of approximately 2.5 million people.
6
+ class Place < BaseRecord
7
+
8
+ include Demographics
9
+
10
+ self.primary_key = "geoid"
11
+
12
+ has_many :zcta_places, foreign_key: :place_geoid, inverse_of: :place, dependent: :destroy
13
+ has_many :zctas, through: :zcta_places
14
+
15
+ has_many :place_counties, foreign_key: :place_geoid, inverse_of: :place, dependent: :destroy
16
+ has_many :counties, through: :place_counties
17
+
18
+ belongs_to :primary_county, foreign_key: :primary_county_geoid, class_name: "USGeo::County"
19
+ belongs_to :urban_area, foreign_key: :urban_area_geoid, optional: true, class_name: "USGeo::UrbanArea"
20
+ belongs_to :state, foreign_key: :state_code, inverse_of: :places
21
+
22
+ validates :geoid, length: {is: 7}
23
+ validates :state_code, length: {is: 2}
24
+ validates :primary_county_geoid, length: {is: 5}
25
+ validates :urban_area_geoid, length: {is: 5}, allow_nil: true
26
+ validates :name, length: {maximum: 60}
27
+ validates :short_name, length: {maximum: 60}
28
+ validates :fips_class_code, length: {is: 2}
29
+ validates :land_area, numericality: true, allow_nil: true
30
+ validates :water_area, numericality: true, allow_nil: true
31
+ validates :population, numericality: {only_integer: true}, allow_nil: true
32
+ validates :housing_units, numericality: {only_integer: true}, allow_nil: true
33
+
34
+ class << self
35
+ def load!(uri = nil)
36
+ location = data_uri(uri || "places.csv")
37
+
38
+ import! do
39
+ load_data_file(location) do |row|
40
+ load_record!(geoid: row["GEOID"]) do |record|
41
+ record.gnis_id = row["GNIS ID"]
42
+ record.name = row["Name"]
43
+ record.short_name = row["Short Name"]
44
+ record.state_code = row["State"]
45
+ record.primary_county_geoid = row["County GEOID"]
46
+ record.urban_area_geoid = row["Urban Area GEOID"]
47
+ record.fips_class_code = row["FIPS Class"]
48
+ record.population = row["Population"]
49
+ record.housing_units = row["Housing Units"]
50
+ record.land_area = area_meters_to_miles(row["Land Area"])
51
+ record.water_area = area_meters_to_miles(row["Water Area"])
52
+ record.lat = row["Latitude"]
53
+ record.lng = row["Longitude"]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # Mapping of urban areas to counties they overlap with.
6
+ class PlaceCounty < BaseRecord
7
+
8
+ belongs_to :county, foreign_key: :county_geoid, inverse_of: :place_counties
9
+ belongs_to :place, foreign_key: :place_geoid, inverse_of: :place_counties
10
+
11
+ validates :county_geoid, length: {is: 5}
12
+ validates :place_geoid, length: {is: 7}
13
+
14
+ class << self
15
+ def load!(uri = nil)
16
+ location = data_uri(uri || "place_counties.csv")
17
+
18
+ import! do
19
+ load_data_file(location) do |row|
20
+ load_record!(place_geoid: row["Place GEOID"], county_geoid: row["County GEOID"]) do |record|
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USGeo
4
+
5
+ # U.S. region.
6
+ class Region < BaseRecord
7
+
8
+ has_many :divisions, inverse_of: :region
9
+ has_many :states, inverse_of: :region
10
+
11
+ validates :name, length: {maximum: 30}
12
+
13
+ class << self
14
+ def load!(uri = nil)
15
+ location = data_uri(uri || "divisions.csv")
16
+
17
+ import! do
18
+ load_data_file(location) do |row|
19
+ load_record!(id: row["Region ID"]) do |record|
20
+ record.name = row["Region Name"]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+ end