Class: Parse::Polygon

Inherits:
Model
  • Object
show all
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.

Examples:

class Region < Parse::Object
  property :area, :polygon
end

# Three accepted constructor forms
triangle = Parse::Polygon.new [[0, 0], [0, 1], [1, 0]]
triangle = Parse::Polygon.new [
  Parse::GeoPoint.new(0, 0),
  Parse::GeoPoint.new(0, 1),
  Parse::GeoPoint.new(1, 0),
]
copy = Parse::Polygon.new(triangle)

region = Region.new
region.area = triangle
region.save

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

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model

#dirty?, find_class

Methods included from Client::Connectable

#client

Constructor Details

#initialize(value = nil) ⇒ Polygon

The initializer accepts an array of [lat, lng] pairs, an array of GeoPoint objects, or another Parse::Polygon.

Examples:

Parse::Polygon.new [[0, 0], [0, 1], [1, 0]]
Parse::Polygon.new [Parse::GeoPoint.new(0, 0), Parse::GeoPoint.new(0, 1), Parse::GeoPoint.new(1, 0)]
Parse::Polygon.new(other_polygon)

Parameters:



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

#coordinatesArray<Array<Float>>

Returns the polygon ring as an array of [lat, lng] pairs.

Returns:

  • (Array<Array<Float>>)

    the polygon ring as an array of [lat, lng] pairs.



54
55
56
# File 'lib/parse/model/polygon.rb', line 54

def coordinates
  @coordinates
end

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.

Examples:

Parse::Polygon.from_geojson("type" => "Polygon", "coordinates" => [[[-117.6, 32.8], [-117.5, 32.8], [-117.5, 32.9], [-117.6, 32.8]]])

Parameters:

  • geojson (Hash)

    a GeoJSON Polygon geometry object.

Returns:

Raises:

  • (ArgumentError)

    if the input is not a valid GeoJSON Polygon.



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.

Examples:

Parse::Polygon.from_points([0, 0], [0, 1], [1, 0])
Parse::Polygon.from_points(gp1, gp2, gp3)

Returns:



75
76
77
# File 'lib/parse/model/polygon.rb', line 75

def self.from_points(*points)
  new(points)
end

.parse_classParse::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

#areaFloat

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.

Returns:

  • (Float)

    non-negative planar area.



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.

Returns:

  • (Hash)

    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

#attributesHash

Returns the attribute hint hash used by the JSON serializer.

Returns:

  • (Hash)

    the attribute hint hash used by the JSON serializer.



144
145
146
# File 'lib/parse/model/polygon.rb', line 144

def attributes
  ATTRIBUTES
end

#boundsArray<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.

Returns:



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

#centroidArray<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.

Returns:



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).

Parameters:

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.

Yield Parameters:

Returns:

  • (Enumerator)

    if no block is given.



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.

Returns:



322
323
324
325
# File 'lib/parse/model/polygon.rb', line 322

def ensure_counter_clockwise!
  @coordinates.reverse! unless counter_clockwise?
  self
end

#geo_pointsArray<Parse::GeoPoint>

Returns the vertices as GeoPoint objects.

Returns:



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_classParse::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_aArray<Array<Float>>

Returns the coordinates in ‘[[lat, lng], …]` form.

Returns:

  • (Array<Array<Float>>)

    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_geojsonHash

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.

Examples:

polygon.to_geojson
# => {"type" => "Polygon", "coordinates" => [[[lng, lat], [lng, lat], ...]]}

Returns:

  • (Hash)

    a GeoJSON ‘Polygon` geometry object.



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_wktString

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.

Returns:

  • (String)

    the WKT string, suitable for PostGIS ‘ST_GeomFromText`.



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