us_geo 1.0.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.
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