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::SYSTEM_CLASS_MAP, 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
Polygongeometry 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
truewhen 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$geoWithinand$geoIntersectsagainst2dsphereindexes). -
#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
#dirty?, find_class, same_parse_class?
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 |
# 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.
260 261 262 263 |
# File 'lib/parse/model/polygon.rb', line 260 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.
185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/parse/model/polygon.rb', line 185 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.
152 153 154 |
# File 'lib/parse/model/polygon.rb', line 152 def as_json(*_args) { __type: parse_class, coordinates: @coordinates.map(&:dup) } end |
#attributes ⇒ Hash
Returns the attribute hint hash used by the JSON serializer.
142 143 144 |
# File 'lib/parse/model/polygon.rb', line 142 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.
174 175 176 177 178 179 |
# File 'lib/parse/model/polygon.rb', line 174 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.
201 202 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 |
# File 'lib/parse/model/polygon.rb', line 201 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).
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/parse/model/polygon.rb', line 270 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.
302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/parse/model/polygon.rb', line 302 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.
165 166 167 168 169 |
# File 'lib/parse/model/polygon.rb', line 165 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.
319 320 321 322 |
# File 'lib/parse/model/polygon.rb', line 319 def ensure_counter_clockwise! @coordinates.reverse! unless counter_clockwise? self end |
#geo_points ⇒ Array<Parse::GeoPoint>
Returns the vertices as GeoPoint objects.
157 158 159 |
# File 'lib/parse/model/polygon.rb', line 157 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.
147 148 149 |
# File 'lib/parse/model/polygon.rb', line 147 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.
239 240 241 242 243 244 |
# File 'lib/parse/model/polygon.rb', line 239 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.
250 251 252 253 254 255 |
# File 'lib/parse/model/polygon.rb', line 250 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 |