Class: Parse::Polygon
- Includes:
- Enumerable
- Defined in:
- lib/parse/model/polygon.rb
Overview
This class manages the Polygon data type that Parse Server provides to store geographic shapes. To define a Polygon property, use the ‘:polygon` data type. A polygon must contain at least three distinct vertices. Each coordinate pair is in `[latitude, longitude]` order (Parse-style), matching GeoPoint and the Parse REST wire format.
The ring is not auto-closed by this class; Parse Server will close it on persist. Equality (‘==`) is element-wise, so an open ring and the same ring with its first point repeated at the end compare as different.
Constant Summary collapse
- ATTRIBUTES =
The default attributes in a Parse Polygon hash. The values are type hints used by the serializer; the keys are the serialized field names.
{ __type: :string, coordinates: :array }.freeze
- MIN_VERTICES =
The minimum number of distinct vertices required by Parse Server.
3
Constants inherited from Model
Model::CLASS_AUDIENCE, Model::CLASS_INSTALLATION, Model::CLASS_JOB_SCHEDULE, Model::CLASS_JOB_STATUS, Model::CLASS_PRODUCT, Model::CLASS_PUSH_STATUS, Model::CLASS_ROLE, Model::CLASS_SCHEMA, Model::CLASS_SESSION, Model::CLASS_USER, Model::ID, Model::KEY_CLASS_NAME, Model::KEY_CREATED_AT, Model::KEY_OBJECT_ID, Model::KEY_UPDATED_AT, Model::OBJECT_ID, Model::TYPE_ACL, Model::TYPE_BYTES, Model::TYPE_DATE, Model::TYPE_FIELD, Model::TYPE_FILE, Model::TYPE_GEOPOINT, Model::TYPE_NUMBER, Model::TYPE_OBJECT, Model::TYPE_POINTER, Model::TYPE_POLYGON, Model::TYPE_RELATION
Instance Attribute Summary collapse
-
#coordinates ⇒ Array<Array<Float>>
The polygon ring as an array of [lat, lng] pairs.
Class Method Summary collapse
-
.from_geojson(geojson) ⇒ Parse::Polygon
Build a Polygon from a GeoJSON ‘Polygon` geometry object.
-
.from_points(*points) ⇒ Parse::Polygon
Convenience factory accepting vertices as positional arguments.
- .parse_class ⇒ Parse::Model::TYPE_POLYGON
Instance Method Summary collapse
-
#==(other) ⇒ Object
Element-wise equality.
-
#area ⇒ Float
Planar area in degrees-squared, computed via the shoelace formula.
-
#as_json(*_args) ⇒ Hash
The Parse REST wire representation of this polygon.
-
#attributes ⇒ Hash
The attribute hint hash used by the JSON serializer.
-
#bounds ⇒ Array<Array<Float>>?
The axis-aligned bounding box of the polygon as ‘[[min_lat, min_lng], [max_lat, max_lng]]`.
-
#centroid ⇒ Array<Float>?
Shoelace-weighted polygon centroid in ‘[lat, lng]` form.
-
#contains_point?(point) ⇒ Boolean
Client-side ray-casting point-in-polygon test.
-
#counter_clockwise? ⇒ Boolean
Returns ‘true` when the outer ring is wound counter-clockwise (as required by RFC 7946 / GeoJSON for exterior rings, and by MongoDB 8+ / Atlas for polygons used in `$geoWithin` and `$geoIntersects` against `2dsphere` indexes).
-
#each {|point| ... } ⇒ Enumerator
Yield each vertex as a GeoPoint.
-
#ensure_counter_clockwise! ⇒ Parse::Polygon
Reverses the coordinate ring in place if it is currently wound clockwise so the polygon satisfies the RFC 7946 / MongoDB 8+ counter-clockwise outer-ring requirement.
-
#geo_points ⇒ Array<Parse::GeoPoint>
The vertices as GeoPoint objects.
- #initialize(value = nil) ⇒ Polygon constructor
- #parse_class ⇒ Parse::Model::TYPE_POLYGON (also: #__type)
-
#to_a ⇒ Array<Array<Float>>
The coordinates in ‘[[lat, lng], …]` form.
-
#to_geojson ⇒ Hash
GeoJSON (RFC 7946) representation of this polygon.
-
#to_wkt ⇒ String
Well-Known Text representation (‘POLYGON((lng lat, lng lat, …))`).
Methods inherited from Model
Methods included from Client::Connectable
Constructor Details
#initialize(value = nil) ⇒ Polygon
The initializer accepts an array of [lat, lng] pairs, an array of GeoPoint objects, or another Parse::Polygon.
64 65 66 67 |
# File 'lib/parse/model/polygon.rb', line 64 def initialize(value = nil) @coordinates = [] self.coordinates = value unless value.nil? end |
Instance Attribute Details
Class Method Details
.from_geojson(geojson) ⇒ Parse::Polygon
Build a Parse::Polygon from a GeoJSON ‘Polygon` geometry object. GeoJSON uses `[longitude, latitude]` axis order and wraps the ring one level deeper than Parse’s wire format; this method performs both transformations. Accepts a closed or open outer ring; the closing vertex (when present and equal to the first) is preserved. Only the outer ring is consumed — GeoJSON inner rings (holes) are silently dropped because Parse Server’s Polygon type does not support holes.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/parse/model/polygon.rb', line 92 def self.from_geojson(geojson) raise ArgumentError, "[Parse::Polygon] from_geojson expects a Hash." unless geojson.is_a?(Hash) hash = geojson.respond_to?(:symbolize_keys) ? geojson.symbolize_keys : geojson type = hash[:type] || hash["type"] rings = hash[:coordinates] || hash["coordinates"] unless type.to_s == "Polygon" && rings.is_a?(Array) && rings.first.is_a?(Array) raise ArgumentError, "[Parse::Polygon] from_geojson expects a GeoJSON Polygon with a nested coordinates array." end outer = rings.first pairs = outer.map do |(lng, lat)| raise ArgumentError, "[Parse::Polygon] GeoJSON ring entries must be [lng, lat] numeric pairs." \ unless lng.is_a?(Numeric) && lat.is_a?(Numeric) [lat.to_f, lng.to_f] end new(pairs) end |
.from_points(*points) ⇒ Parse::Polygon
Convenience factory accepting vertices as positional arguments. Each argument may be a GeoPoint or a ‘[lat, lng]` pair.
75 76 77 |
# File 'lib/parse/model/polygon.rb', line 75 def self.from_points(*points) new(points) end |
.parse_class ⇒ Parse::Model::TYPE_POLYGON
47 |
# File 'lib/parse/model/polygon.rb', line 47 def self.parse_class; TYPE_POLYGON; end |
Instance Method Details
#==(other) ⇒ Object
Element-wise equality. Two polygons are equal if their coordinate arrays match exactly. An open ring and its closed form are NOT equal, matching the JS SDK.
262 263 264 265 |
# File 'lib/parse/model/polygon.rb', line 262 def ==(other) return false unless other.is_a?(Parse::Polygon) @coordinates == other.coordinates end |
#area ⇒ Float
Planar area in degrees-squared, computed via the shoelace formula. This is a Cartesian approximation and is useful for relative comparison only. For surface-area in square meters use a proper geodesic library.
187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/parse/model/polygon.rb', line 187 def area return 0.0 if @coordinates.length < MIN_VERTICES sum = 0.0 n = @coordinates.length n.times do |i| lat_i, lng_i = @coordinates[i] lat_j, lng_j = @coordinates[(i + 1) % n] sum += (lng_i * lat_j) - (lng_j * lat_i) end (sum.abs / 2.0) end |
#as_json(*_args) ⇒ Hash
Returns the Parse REST wire representation of this polygon.
154 155 156 |
# File 'lib/parse/model/polygon.rb', line 154 def as_json(*_args) { __type: parse_class, coordinates: @coordinates.map(&:dup) } end |
#attributes ⇒ Hash
Returns the attribute hint hash used by the JSON serializer.
144 145 146 |
# File 'lib/parse/model/polygon.rb', line 144 def attributes ATTRIBUTES end |
#bounds ⇒ Array<Array<Float>>?
The axis-aligned bounding box of the polygon as ‘[[min_lat, min_lng], [max_lat, max_lng]]`. Returns `nil` for an empty polygon.
176 177 178 179 180 181 |
# File 'lib/parse/model/polygon.rb', line 176 def bounds return nil if @coordinates.empty? lats = @coordinates.map(&:first) lngs = @coordinates.map(&:last) [[lats.min, lngs.min], [lats.max, lngs.max]] end |
#centroid ⇒ Array<Float>?
Shoelace-weighted polygon centroid in ‘[lat, lng]` form. Falls back to the vertex average when the polygon has zero area (e.g. a degenerate ring of collinear points). Returns `nil` for an empty polygon.
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/parse/model/polygon.rb', line 203 def centroid return nil if @coordinates.empty? n = @coordinates.length return @coordinates.first.dup if n == 1 sum_a = 0.0 sum_lat = 0.0 sum_lng = 0.0 n.times do |i| lat_i, lng_i = @coordinates[i] lat_j, lng_j = @coordinates[(i + 1) % n] cross = (lng_i * lat_j) - (lng_j * lat_i) sum_a += cross sum_lat += (lat_i + lat_j) * cross sum_lng += (lng_i + lng_j) * cross end if sum_a.abs < 1e-12 # Degenerate ring — fall back to vertex average so callers always # get a usable point. lat = @coordinates.map(&:first).sum / n lng = @coordinates.map(&:last).sum / n return [lat, lng] end factor = 1.0 / (3.0 * sum_a) [sum_lat * factor, sum_lng * factor] end |
#contains_point?(point) ⇒ Boolean
Client-side ray-casting point-in-polygon test. Mirrors ‘Parse.Polygon#containsPoint` in the JS SDK. Boundary behavior is not guaranteed (a point exactly on an edge may return either result).
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/parse/model/polygon.rb', line 272 def contains_point?(point) lat, lng = case point when Parse::GeoPoint then [point.latitude, point.longitude] when Array then [point[0].to_f, point[1].to_f] else raise ArgumentError, "[Parse::Polygon] contains_point? expects a Parse::GeoPoint or [lat,lng] Array." end ring = @coordinates return false if ring.size < MIN_VERTICES inside = false j = ring.size - 1 (0...ring.size).each do |i| lat_i, lng_i = ring[i] lat_j, lng_j = ring[j] intersect = ((lng_i > lng) != (lng_j > lng)) && (lat < (lat_j - lat_i) * (lng - lng_i) / ((lng_j - lng_i).nonzero? || 1e-12) + lat_i) inside = !inside if intersect j = i end inside end |
#counter_clockwise? ⇒ Boolean
Returns ‘true` when the outer ring is wound counter-clockwise (as required by RFC 7946 / GeoJSON for exterior rings, and by MongoDB 8+ / Atlas for polygons used in `$geoWithin` and `$geoIntersects` against `2dsphere` indexes). Uses the shoelace signed-area test with longitude on the x-axis and latitude on the y-axis. Degenerate rings (fewer than MIN_VERTICES vertices) return `true` because winding is undefined.
305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/parse/model/polygon.rb', line 305 def counter_clockwise? n = @coordinates.length return true if n < MIN_VERTICES sum = 0.0 n.times do |i| lat_i, lng_i = @coordinates[i] lat_j, lng_j = @coordinates[(i + 1) % n] sum += (lng_i * lat_j) - (lng_j * lat_i) end sum > 0 end |
#each {|point| ... } ⇒ Enumerator
Yield each vertex as a GeoPoint. Including Enumerable gives ‘map`, `select`, `to_a`, etc. for free.
167 168 169 170 171 |
# File 'lib/parse/model/polygon.rb', line 167 def each(&block) return enum_for(:each) unless block_given? @coordinates.each { |(lat, lng)| yield Parse::GeoPoint.new(lat, lng) } self end |
#ensure_counter_clockwise! ⇒ Parse::Polygon
Reverses the coordinate ring in place if it is currently wound clockwise so the polygon satisfies the RFC 7946 / MongoDB 8+ counter-clockwise outer-ring requirement. Returns ‘self` so calls chain. Idempotent: calling on an already-CCW polygon is a no-op.
322 323 324 325 |
# File 'lib/parse/model/polygon.rb', line 322 def ensure_counter_clockwise! @coordinates.reverse! unless counter_clockwise? self end |
#geo_points ⇒ Array<Parse::GeoPoint>
Returns the vertices as GeoPoint objects.
159 160 161 |
# File 'lib/parse/model/polygon.rb', line 159 def geo_points @coordinates.map { |(lat, lng)| Parse::GeoPoint.new(lat, lng) } end |
#parse_class ⇒ Parse::Model::TYPE_POLYGON Also known as: __type
49 |
# File 'lib/parse/model/polygon.rb', line 49 def parse_class; self.class.parse_class; end |
#to_a ⇒ Array<Array<Float>>
Returns the coordinates in ‘[[lat, lng], …]` form.
149 150 151 |
# File 'lib/parse/model/polygon.rb', line 149 def to_a @coordinates.map(&:dup) end |
#to_geojson ⇒ Hash
GeoJSON (RFC 7946) representation of this polygon. GeoJSON requires ‘[longitude, latitude]` axis order (the inverse of Parse) and a closed ring nested one level deeper than Parse’s wire format. This method performs both transformations so the result drops directly into Leaflet, Mapbox, PostGIS, and other standard GIS tools.
241 242 243 244 245 246 |
# File 'lib/parse/model/polygon.rb', line 241 def to_geojson ring = @coordinates.map { |(lat, lng)| [lng, lat] } # GeoJSON requires the ring to be explicitly closed. ring << ring.first.dup if !ring.empty? && ring.first != ring.last { "type" => "Polygon", "coordinates" => [ring] } end |
#to_wkt ⇒ String
Well-Known Text representation (‘POLYGON((lng lat, lng lat, …))`). The output uses `longitude latitude` axis order — matching the OGC WKT spec — and includes the closing vertex if not already present.
252 253 254 255 256 257 |
# File 'lib/parse/model/polygon.rb', line 252 def to_wkt return "POLYGON EMPTY" if @coordinates.empty? ring = @coordinates.map { |(lat, lng)| [lng, lat] } ring << ring.first.dup if ring.first != ring.last "POLYGON((#{ring.map { |(lng, lat)| "#{lng} #{lat}" }.join(", ")}))" end |