Class: Metro

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/has_geo_lookup/models/metro.rb

Overview

Metropolitan areas defined by collections of administrative boundaries

This model represents metropolitan areas (metro areas, urban agglomerations) as collections of administrative boundaries from GeoBoundaries.org. Rather than storing separate geometry, metros are defined by their constituent geoboundaries, allowing for flexible and maintainable metro definitions.

Metro areas are useful for:

  • Real estate market analysis by metropolitan region
  • Economic data aggregation across municipal boundaries
  • Transportation and infrastructure planning
  • Population and demographic analysis

Each metro can span multiple administrative levels and boundaries, reflecting the real-world nature of metropolitan areas that often cross county, city, and sometimes state boundaries.

Examples:

Find metros containing a point

Metro.joins(:geoboundaries)
     .where("ST_Contains(geoboundaries.boundary, ST_GeomFromText(?, 4326))", "POINT(-122.4194 37.7749)")

Find metros in a specific country

Metro.where(country_code: "US")

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#country_codeString

Primary country code for this metro area

Returns:

  • (String)

    the current value of country_code



31
32
33
# File 'lib/has_geo_lookup/models/metro.rb', line 31

def country_code
  @country_code
end

#detailsString

Additional descriptive information about the metro

Returns:

  • (String)

    the current value of details



31
32
33
# File 'lib/has_geo_lookup/models/metro.rb', line 31

def details
  @details
end

#nameString

Metro area name (e.g., "San Francisco Bay Area", "Greater London")

Returns:

  • (String)

    the current value of name



31
32
33
# File 'lib/has_geo_lookup/models/metro.rb', line 31

def name
  @name
end

#populationInteger

Estimated metro population (optional)

Returns:

  • (Integer)

    the current value of population



31
32
33
# File 'lib/has_geo_lookup/models/metro.rb', line 31

def population
  @population
end

Instance Method Details

#admin_levelsArray<String>

Get administrative levels represented in this metro

Returns the different administrative levels (ADM1, ADM2, etc.) that make up this metropolitan area.

Examples:

metro.admin_levels
# => ["ADM1", "ADM2"] (includes state and county level boundaries)

Returns:

  • (Array<String>)

    Array of administrative levels



155
156
157
# File 'lib/has_geo_lookup/models/metro.rb', line 155

def admin_levels
  geoboundaries.distinct.pluck(:level).compact.sort
end

#boundary_namesArray<String>

Get all boundary names that define this metro

Returns a list of all constituent boundary names for understanding the geographic composition of this metropolitan area.

Examples:

metro.boundary_names
# => ["San Francisco County", "Alameda County", "Santa Clara County", ...]

Returns:

  • (Array<String>)

    Array of boundary names



141
142
143
# File 'lib/has_geo_lookup/models/metro.rb', line 141

def boundary_names
  geoboundaries.pluck(:name).compact.sort
end

#centroidArray<Float>?

Get the geographic center (centroid) of this metro

Calculates the centroid of all constituent boundaries combined, providing a representative center point for the metropolitan area.

Examples:

metro.centroid
# => [37.4419, -122.1430] (lat, lng of metro center)

Returns:

  • (Array<Float>, nil)

    [latitude, longitude] of metro center, or nil if error



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/has_geo_lookup/models/metro.rb', line 114

def centroid
  return nil unless geoboundaries.any?

  result = self.class.connection.select_one(<<~SQL.squish)
    SELECT 
      ST_Y(ST_Centroid(ST_Union(boundary))) AS lat,
      ST_X(ST_Centroid(ST_Union(boundary))) AS lng
    FROM geoboundaries 
    WHERE id IN (#{geoboundary_ids.join(',')})
  SQL

  result ? [result['lat'].to_f, result['lng'].to_f] : nil
rescue => e
  Rails.logger.warn "Error calculating metro centroid: #{e.message}"
  nil
end

#contains_point?(latitude, longitude) ⇒ Boolean

Check if this metro contains the given coordinates

Uses MySQL spatial queries against all associated geoboundaries to determine if the point falls within any boundary that defines this metropolitan area.

Examples:

metro.contains_point?(37.7749, -122.4194)
# => true (if coordinates are within San Francisco Bay Area)

Parameters:

  • latitude (Float)

    Latitude in decimal degrees

  • longitude (Float)

    Longitude in decimal degrees

Returns:

  • (Boolean)

    true if the point is within this metro area



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/has_geo_lookup/models/metro.rb', line 64

def contains_point?(latitude, longitude)
  return false unless latitude && longitude
  return false unless geoboundaries.any?

  # Check if point is contained within any of the metro's boundaries
  geoboundaries.joins("INNER JOIN geoboundaries gb ON gb.id = geoboundaries.id")
               .where("ST_Contains(gb.boundary, ST_GeomFromText(?, 4326))", 
                      "POINT(#{latitude} #{longitude})")
               .exists?
rescue => e
  Rails.logger.warn "Error checking metro point containment: #{e.message}"
  false
end

#display_nameString

Returns a human-readable description of this metro

Combines name, details, country, and constituent boundary information for comprehensive metro identification.

Examples:

metro.display_name
# => "San Francisco Bay Area (US) - 9 counties, 2 admin levels"

Returns:

  • (String)

    Formatted description



201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/has_geo_lookup/models/metro.rb', line 201

def display_name
  parts = [name]
  parts << "(#{country_code})" if country_code.present?
  
  if geoboundaries.any?
    boundary_count = geoboundaries.count
    level_count = admin_levels.count
    parts << "#{boundary_count} boundaries, #{level_count} admin level#{'s' if level_count != 1}"
  end
  
  description = parts.join(" - ")
  description += "\n#{details}" if details.present?
  description
end

#geographic_summaryHash

Get a summary of this metro's geographic composition

Returns detailed information about the boundaries and administrative levels that make up this metropolitan area.

Examples:

metro.geographic_summary
# => {
#   total_boundaries: 9,
#   by_level: {"ADM1" => 1, "ADM2" => 8},
#   boundary_names: ["San Francisco County", "Alameda County", ...],
#   spans_multiple_states: false
# }

Returns:

  • (Hash)

    Summary with boundary counts by level and names



236
237
238
239
240
241
242
243
244
245
246
# File 'lib/has_geo_lookup/models/metro.rb', line 236

def geographic_summary
  {
    total_boundaries: geoboundaries.count,
    by_level: geoboundaries.group(:level).count,
    boundary_names: boundary_names,
    spans_multiple_states: multi_state?,
    admin_levels: admin_levels,
    total_area_km2: total_area_km2,
    population_density: population_density
  }
end

#multi_state?Boolean

Check if this metro spans multiple states/provinces

Determines if the metro includes boundaries from multiple ADM1 (state/province) level divisions.

Examples:

metro.multi_state?
# => true (if metro crosses state boundaries)

Returns:

  • (Boolean)

    true if metro spans multiple states/provinces



169
170
171
172
# File 'lib/has_geo_lookup/models/metro.rb', line 169

def multi_state?
  state_boundaries = geoboundaries.where(level: "ADM1")
  state_boundaries.count > 1
end

#population_densityFloat?

Get population density (people per km²)

Calculates population density based on total population and area, if both values are available.

Examples:

metro.population_density
# => 1547.3 (people per square kilometer)

Returns:

  • (Float, nil)

    Population density in people per km², or nil if data unavailable



184
185
186
187
188
189
# File 'lib/has_geo_lookup/models/metro.rb', line 184

def population_density
  return nil unless population && total_area_km2
  return nil if total_area_km2.zero?
  
  (population.to_f / total_area_km2).round(1)
end

#total_area_km2Float?

Calculate the total area of this metro in square kilometers

Sums the areas of all constituent geoboundaries, with handling for overlapping boundaries to avoid double-counting.

Examples:

metro.total_area_km2
# => 18040.5 (total metro area in square kilometers)

Returns:

  • (Float, nil)

    Total area in square kilometers, or nil if no boundaries



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/has_geo_lookup/models/metro.rb', line 88

def total_area_km2
  return nil unless geoboundaries.any?

  # Calculate total area using ST_Union to handle overlapping boundaries
  result = self.class.connection.select_value(<<~SQL.squish)
    SELECT ST_Area(ST_Transform(ST_Union(boundary), 3857)) / 1000000 AS total_area_km2
    FROM geoboundaries 
    WHERE id IN (#{geoboundary_ids.join(',')})
  SQL

  result&.round(2)
rescue => e
  Rails.logger.warn "Error calculating metro area: #{e.message}"
  nil
end