parse-stack-next 4.5.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.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "model"
|
|
5
|
+
require_relative "geopoint"
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
# GeoJSON-native geometry wrappers for types that Parse Server's schema
|
|
9
|
+
# does NOT model directly but that MongoDB's `2dsphere` index supports
|
|
10
|
+
# natively. These classes are designed for callers that go through the
|
|
11
|
+
# mongo-direct surface (`Parse::MongoDB`) or Atlas Search, where stored
|
|
12
|
+
# geometry can be richer than the `GeoPoint` / `Polygon` types Parse
|
|
13
|
+
# Server exposes.
|
|
14
|
+
#
|
|
15
|
+
# **Axis order.** Unlike {Parse::GeoPoint} and {Parse::Polygon}, which
|
|
16
|
+
# store coordinates in Parse-native `[latitude, longitude]` order to
|
|
17
|
+
# match the REST wire format, every class under `Parse::GeoJSON` stores
|
|
18
|
+
# coordinates in GeoJSON-native `[longitude, latitude]` order. The
|
|
19
|
+
# namespace itself is the axis-order signal — pick the namespace based
|
|
20
|
+
# on which side of the boundary you're working on.
|
|
21
|
+
#
|
|
22
|
+
# **Storage.** These geometries live in `:object` Parse columns. Parse
|
|
23
|
+
# Server treats the value as an opaque hash on read and write; MongoDB
|
|
24
|
+
# will happily index it on a `2dsphere` index regardless of whether
|
|
25
|
+
# Parse Server's schema knows the type exists.
|
|
26
|
+
module GeoJSON
|
|
27
|
+
# Base class for GeoJSON geometry wrappers. Subclasses define `TYPE`
|
|
28
|
+
# and `#valid_coordinates?` and inherit the round-trip plumbing.
|
|
29
|
+
class Geometry < Parse::Model
|
|
30
|
+
# @return [Array] the raw coordinates array, in GeoJSON nesting and
|
|
31
|
+
# `[longitude, latitude]` axis order. Shape varies by subclass.
|
|
32
|
+
attr_reader :coordinates
|
|
33
|
+
|
|
34
|
+
# @return [String] the GeoJSON `type` discriminator (`"Point"`,
|
|
35
|
+
# `"LineString"`, `"Polygon"`, `"MultiPolygon"`, etc.).
|
|
36
|
+
def self.geojson_type
|
|
37
|
+
const_get(:TYPE)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def geojson_type
|
|
41
|
+
self.class.geojson_type
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The initializer accepts either a GeoJSON Hash (`{type:, coordinates:}`),
|
|
45
|
+
# a plain coordinates Array, or another instance of the same class.
|
|
46
|
+
def initialize(value = nil)
|
|
47
|
+
@coordinates = []
|
|
48
|
+
self.coordinates = value unless value.nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def coordinates=(value)
|
|
52
|
+
coords =
|
|
53
|
+
case value
|
|
54
|
+
when self.class
|
|
55
|
+
deep_copy_array(value.coordinates)
|
|
56
|
+
when Hash
|
|
57
|
+
hash = value.respond_to?(:symbolize_keys) ? value.symbolize_keys : value
|
|
58
|
+
type = hash[:type] || hash["type"]
|
|
59
|
+
if type && type.to_s != self.class.geojson_type
|
|
60
|
+
raise ArgumentError, "[#{self.class}] expected GeoJSON type " \
|
|
61
|
+
"#{self.class.geojson_type.inspect}, got #{type.inspect}."
|
|
62
|
+
end
|
|
63
|
+
normalize(hash[:coordinates] || hash["coordinates"] || [])
|
|
64
|
+
when Array
|
|
65
|
+
normalize(value)
|
|
66
|
+
else
|
|
67
|
+
raise ArgumentError, "[#{self.class}] cannot build from #{value.class}: " \
|
|
68
|
+
"expected GeoJSON Hash, coordinates Array, or another #{self.class}."
|
|
69
|
+
end
|
|
70
|
+
@coordinates = coords
|
|
71
|
+
validate!
|
|
72
|
+
@coordinates
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Hash] the standard GeoJSON `{type:, coordinates:}` hash.
|
|
76
|
+
def to_geojson
|
|
77
|
+
{ "type" => geojson_type, "coordinates" => deep_copy_array(@coordinates) }
|
|
78
|
+
end
|
|
79
|
+
alias_method :as_json, :to_geojson
|
|
80
|
+
|
|
81
|
+
# @return [String] the JSON form, suitable for direct shipment to
|
|
82
|
+
# any GeoJSON-aware consumer.
|
|
83
|
+
def to_json(*args)
|
|
84
|
+
to_geojson.to_json(*args)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ==(other)
|
|
88
|
+
return false unless other.is_a?(self.class)
|
|
89
|
+
@coordinates == other.coordinates
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def inspect
|
|
93
|
+
"#<#{self.class.name} #{@coordinates.inspect}>"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @!visibility private
|
|
97
|
+
def initialize_copy(other)
|
|
98
|
+
super
|
|
99
|
+
@coordinates = deep_copy_array(other.coordinates)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Build any GeoJSON geometry from its wire-format Hash. Dispatches to
|
|
103
|
+
# the matching subclass based on the `type` field.
|
|
104
|
+
# @example
|
|
105
|
+
# Parse::GeoJSON::Geometry.from_geojson(line_string_hash) # => Parse::GeoJSON::LineString
|
|
106
|
+
# @return [Parse::GeoJSON::Geometry]
|
|
107
|
+
def self.from_geojson(hash)
|
|
108
|
+
raise ArgumentError, "[Parse::GeoJSON::Geometry] expected a Hash." unless hash.is_a?(Hash)
|
|
109
|
+
h = hash.respond_to?(:symbolize_keys) ? hash.symbolize_keys : hash
|
|
110
|
+
type = (h[:type] || h["type"]).to_s
|
|
111
|
+
klass = TYPE_REGISTRY[type]
|
|
112
|
+
raise ArgumentError, "[Parse::GeoJSON::Geometry] unsupported GeoJSON type #{type.inspect}." if klass.nil?
|
|
113
|
+
klass.new(h)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def deep_copy_array(arr)
|
|
119
|
+
arr.map { |entry| entry.is_a?(Array) ? deep_copy_array(entry) : entry }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def normalize(_value)
|
|
123
|
+
raise NotImplementedError, "subclass must implement #normalize"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validate!
|
|
127
|
+
# subclasses may override
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# `LineString` — an ordered sequence of `[longitude, latitude]` points
|
|
132
|
+
# describing a path. The most common applications are GPS tracks,
|
|
133
|
+
# delivery routes, road segments, and trail centerlines.
|
|
134
|
+
#
|
|
135
|
+
# GeoJSON requires ≥ 2 points; this class warns when the constraint
|
|
136
|
+
# is violated rather than raising, matching {Parse::GeoPoint} /
|
|
137
|
+
# {Parse::Polygon} validation style.
|
|
138
|
+
#
|
|
139
|
+
# @example
|
|
140
|
+
# Parse::GeoJSON::LineString.new [[-122.4, 37.7], [-122.39, 37.78]]
|
|
141
|
+
class LineString < Geometry
|
|
142
|
+
TYPE = "LineString"
|
|
143
|
+
MIN_POINTS = 2
|
|
144
|
+
|
|
145
|
+
# @return [Array<Parse::GeoPoint>] the path as Parse::GeoPoint objects
|
|
146
|
+
# (axis-swapped back to Parse's `[lat, lng]`).
|
|
147
|
+
def geo_points
|
|
148
|
+
@coordinates.map { |(lng, lat)| Parse::GeoPoint.new(lat, lng) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def normalize(value)
|
|
154
|
+
raise ArgumentError, "[Parse::GeoJSON::LineString] coordinates must be an Array." unless value.is_a?(Array)
|
|
155
|
+
value.map do |pair|
|
|
156
|
+
case pair
|
|
157
|
+
when Parse::GeoPoint
|
|
158
|
+
finite_lnglat!(pair.longitude.to_f, pair.latitude.to_f)
|
|
159
|
+
when Array
|
|
160
|
+
unless pair.length == 2 && pair[0].is_a?(Numeric) && pair[1].is_a?(Numeric)
|
|
161
|
+
raise ArgumentError, "[Parse::GeoJSON::LineString] each coordinate must be a [lng, lat] numeric pair."
|
|
162
|
+
end
|
|
163
|
+
finite_lnglat!(pair[0].to_f, pair[1].to_f)
|
|
164
|
+
else
|
|
165
|
+
raise ArgumentError, "[Parse::GeoJSON::LineString] unsupported coordinate entry #{pair.inspect}."
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Reject NaN / Infinity at the door. `Float::NAN.is_a?(Numeric)`
|
|
171
|
+
# is true so the earlier type check is insufficient; persisting
|
|
172
|
+
# a line with NaN errors mid-pipeline at `2dsphere` index rebuild
|
|
173
|
+
# time.
|
|
174
|
+
def finite_lnglat!(lng, lat)
|
|
175
|
+
unless lng.finite? && lat.finite?
|
|
176
|
+
raise ArgumentError, "[Parse::GeoJSON::LineString] coordinates must be finite numerics; got [#{lng}, #{lat}]."
|
|
177
|
+
end
|
|
178
|
+
[lng, lat]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def validate!
|
|
182
|
+
return if @coordinates.empty?
|
|
183
|
+
if @coordinates.length < MIN_POINTS
|
|
184
|
+
warn "[Parse::GeoJSON::LineString] requires at least #{MIN_POINTS} points; got #{@coordinates.length}."
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# `MultiPolygon` — an Array of Polygons, each Polygon an Array of
|
|
190
|
+
# linear rings, each ring an Array of `[lng, lat]` pairs. The canonical
|
|
191
|
+
# use case is administrative or territorial regions made up of
|
|
192
|
+
# disjoint pieces (Hawaii, Indonesia, multi-island service areas,
|
|
193
|
+
# postal-code clusters).
|
|
194
|
+
#
|
|
195
|
+
# GeoJSON nesting depth is 4: `coordinates[polygon][ring][point][lng_or_lat]`.
|
|
196
|
+
# Each ring must contain ≥ 4 points and be explicitly closed.
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# Parse::GeoJSON::MultiPolygon.new [
|
|
200
|
+
# [[[ 0, 0], [ 1, 0], [ 1, 1], [ 0, 1], [ 0, 0]]],
|
|
201
|
+
# [[[ 5, 5], [ 6, 5], [ 6, 6], [ 5, 6], [ 5, 5]]],
|
|
202
|
+
# ]
|
|
203
|
+
class MultiPolygon < Geometry
|
|
204
|
+
TYPE = "MultiPolygon"
|
|
205
|
+
MIN_RING_POINTS = 4
|
|
206
|
+
|
|
207
|
+
# @return [Array<Parse::Polygon>] each member polygon as a
|
|
208
|
+
# {Parse::Polygon} (with axis swap back to Parse's `[lat, lng]`).
|
|
209
|
+
# Inner rings (holes) are dropped because {Parse::Polygon} does
|
|
210
|
+
# not support them.
|
|
211
|
+
def polygons
|
|
212
|
+
@coordinates.map do |rings|
|
|
213
|
+
outer = rings.first
|
|
214
|
+
Parse::Polygon.new(outer.map { |(lng, lat)| [lat.to_f, lng.to_f] })
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def normalize(value)
|
|
221
|
+
raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] coordinates must be an Array." unless value.is_a?(Array)
|
|
222
|
+
value.map do |polygon|
|
|
223
|
+
unless polygon.is_a?(Array)
|
|
224
|
+
raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] each polygon must be an Array of rings."
|
|
225
|
+
end
|
|
226
|
+
polygon.map do |ring|
|
|
227
|
+
unless ring.is_a?(Array)
|
|
228
|
+
raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] each ring must be an Array of [lng, lat] pairs."
|
|
229
|
+
end
|
|
230
|
+
ring.map do |pair|
|
|
231
|
+
unless pair.is_a?(Array) && pair.length == 2 &&
|
|
232
|
+
pair[0].is_a?(Numeric) && pair[1].is_a?(Numeric)
|
|
233
|
+
raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] each coordinate must be a [lng, lat] numeric pair."
|
|
234
|
+
end
|
|
235
|
+
# Reject NaN / Infinity; see LineString#finite_lnglat!
|
|
236
|
+
# for the same rationale.
|
|
237
|
+
lng = pair[0].to_f
|
|
238
|
+
lat = pair[1].to_f
|
|
239
|
+
unless lng.finite? && lat.finite?
|
|
240
|
+
raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] coordinates must be finite " \
|
|
241
|
+
"numerics; got [#{lng}, #{lat}]."
|
|
242
|
+
end
|
|
243
|
+
[lng, lat]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def validate!
|
|
250
|
+
@coordinates.each_with_index do |polygon, i|
|
|
251
|
+
polygon.each_with_index do |ring, j|
|
|
252
|
+
next if ring.empty?
|
|
253
|
+
if ring.length < MIN_RING_POINTS
|
|
254
|
+
warn "[Parse::GeoJSON::MultiPolygon] polygon[#{i}].ring[#{j}] has #{ring.length} points; " \
|
|
255
|
+
"GeoJSON requires at least #{MIN_RING_POINTS}."
|
|
256
|
+
elsif ring.first != ring.last
|
|
257
|
+
warn "[Parse::GeoJSON::MultiPolygon] polygon[#{i}].ring[#{j}] is not closed (first != last)."
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Dispatch table for {Geometry.from_geojson}. Kept here at the end so
|
|
265
|
+
# subclasses are registered after their constants are defined.
|
|
266
|
+
Geometry::TYPE_REGISTRY = {
|
|
267
|
+
"LineString" => LineString,
|
|
268
|
+
"MultiPolygon" => MultiPolygon,
|
|
269
|
+
}.freeze
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "model"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
|
|
8
|
+
# This class manages the GeoPoint data type that Parse provides to support
|
|
9
|
+
# geo-queries. To define a GeoPoint property, use the `:geopoint` data type.
|
|
10
|
+
# Please note that latitudes should not be between -90.0 and 90.0, and
|
|
11
|
+
# longitudes should be between -180.0 and 180.0.
|
|
12
|
+
# @example
|
|
13
|
+
# class PlaceObject < Parse::Object
|
|
14
|
+
# property :location, :geopoint
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# san_diego = Parse::GeoPoint.new(32.8233, -117.6542)
|
|
18
|
+
# los_angeles = Parse::GeoPoint.new [34.0192341, -118.970792]
|
|
19
|
+
# san_diego == los_angeles # false
|
|
20
|
+
#
|
|
21
|
+
# place = PlaceObject.new
|
|
22
|
+
# place.location = san_diego
|
|
23
|
+
# place.save
|
|
24
|
+
#
|
|
25
|
+
class GeoPoint < Model
|
|
26
|
+
# The default attributes in a Parse GeoPoint hash.
|
|
27
|
+
ATTRIBUTES = { __type: :string, latitude: :float, longitude: :float }.freeze
|
|
28
|
+
|
|
29
|
+
# @return [Float] latitude value between -90.0 and 90.0
|
|
30
|
+
attr_reader :latitude
|
|
31
|
+
# @return [Float] longitude value between -180.0 and 180.0
|
|
32
|
+
attr_reader :longitude
|
|
33
|
+
# The key field for latitude
|
|
34
|
+
FIELD_LAT = "latitude".freeze
|
|
35
|
+
# The key field for longitude
|
|
36
|
+
FIELD_LNG = "longitude".freeze
|
|
37
|
+
|
|
38
|
+
# The minimum latitude value.
|
|
39
|
+
LAT_MIN = -90.0
|
|
40
|
+
# The maximum latitude value.
|
|
41
|
+
LAT_MAX = 90.0
|
|
42
|
+
# The minimum longitude value.
|
|
43
|
+
LNG_MIN = -180.0
|
|
44
|
+
# The maximum longitude value.
|
|
45
|
+
LNG_MAX = 180.0
|
|
46
|
+
|
|
47
|
+
alias_method :lat, :latitude
|
|
48
|
+
alias_method :lng, :longitude
|
|
49
|
+
# @return [Model::TYPE_GEOPOINT]
|
|
50
|
+
def self.parse_class; TYPE_GEOPOINT; end
|
|
51
|
+
# @return [Model::TYPE_GEOPOINT]
|
|
52
|
+
def parse_class; self.class.parse_class; end
|
|
53
|
+
|
|
54
|
+
alias_method :__type, :parse_class
|
|
55
|
+
|
|
56
|
+
# The initializer can create a GeoPoint with a hash, array or values.
|
|
57
|
+
# @example
|
|
58
|
+
# san_diego = Parse::GeoPoint.new(32.8233, -117.6542)
|
|
59
|
+
# san_diego = Parse::GeoPoint.new [32.8233, -117.6542]
|
|
60
|
+
# san_diego = Parse::GeoPoint.new { latitude: 32.8233, longitude: -117.6542}
|
|
61
|
+
#
|
|
62
|
+
# @param latitude [Numeric] The latitude value between LAT_MIN and LAT_MAX.
|
|
63
|
+
# @param longitude [Numeric] The longitude value between LNG_MIN and LNG_MAX.
|
|
64
|
+
def initialize(latitude = nil, longitude = nil)
|
|
65
|
+
@latitude = @longitude = 0.0
|
|
66
|
+
if latitude.is_a?(Hash) || latitude.is_a?(Array)
|
|
67
|
+
self.attributes = latitude
|
|
68
|
+
elsif latitude.is_a?(Numeric) && longitude.is_a?(Numeric)
|
|
69
|
+
@latitude = latitude
|
|
70
|
+
@longitude = longitude
|
|
71
|
+
elsif latitude.is_a?(GeoPoint)
|
|
72
|
+
@latitude = latitude.latitude
|
|
73
|
+
@longitude = latitude.longitude
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
_validate_point
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @!visibility private
|
|
80
|
+
def _validate_point
|
|
81
|
+
unless @latitude.nil? || @latitude.between?(LAT_MIN, LAT_MAX)
|
|
82
|
+
warn "[Parse::GeoPoint] Latitude (#{@latitude}) is not between #{LAT_MIN}, #{LAT_MAX}!"
|
|
83
|
+
warn "Attempting to use GeoPoint’s with latitudes outside these ranges will raise an exception in a future release."
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
unless @longitude.nil? || @longitude.between?(LNG_MIN, LNG_MAX)
|
|
87
|
+
warn "[Parse::GeoPoint] Longitude (#{@longitude}) is not between #{LNG_MIN}, #{LNG_MAX}!"
|
|
88
|
+
warn "Attempting to use GeoPoint’s with longitude outside these ranges will raise an exception in a future release."
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Hash] attributes for a Parse GeoPoint.
|
|
93
|
+
def attributes
|
|
94
|
+
ATTRIBUTES
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Helper method for performing geo-queries with radial miles constraints
|
|
98
|
+
# @return [Array] containing [lat,lng,miles]
|
|
99
|
+
def max_miles(m)
|
|
100
|
+
m = 0 if m.nil?
|
|
101
|
+
[@latitude, @longitude, m]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Helper method for performing geo-queries with a radial kilometer
|
|
105
|
+
# constraint. Used with `:field.near => gp.max_kilometers(N)` to compile
|
|
106
|
+
# a `$nearSphere` + `$maxDistanceInKilometers` query against Parse Server.
|
|
107
|
+
# @return [Array] containing `[lat, lng, kilometers, :km]`
|
|
108
|
+
def max_kilometers(km)
|
|
109
|
+
km = 0 if km.nil?
|
|
110
|
+
[@latitude, @longitude, km, :km]
|
|
111
|
+
end
|
|
112
|
+
alias_method :max_km, :max_kilometers
|
|
113
|
+
|
|
114
|
+
# Helper method for performing geo-queries with a radial radians
|
|
115
|
+
# constraint. Used with `:field.near => gp.max_radians(R)` to compile
|
|
116
|
+
# a `$nearSphere` + `$maxDistance` query against Parse Server (raw
|
|
117
|
+
# `$maxDistance` is measured in radians). Convert from miles/km by
|
|
118
|
+
# dividing by mean-Earth-radius (~3958.8 miles or ~6371 km).
|
|
119
|
+
# @return [Array] containing `[lat, lng, radians, :radians]`
|
|
120
|
+
def max_radians(rad)
|
|
121
|
+
rad = 0 if rad.nil?
|
|
122
|
+
[@latitude, @longitude, rad, :radians]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def latitude=(l)
|
|
126
|
+
@latitude = l
|
|
127
|
+
_validate_point
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def longitude=(l)
|
|
131
|
+
@longitude = l
|
|
132
|
+
_validate_point
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Setting lat and lng for an GeoPoint can be done using a hash with the attributes set
|
|
136
|
+
# or with an array of two items where the first is the lat and the second is the lng (ex. [32.22,-118.81])
|
|
137
|
+
def attributes=(h)
|
|
138
|
+
if h.is_a?(Hash)
|
|
139
|
+
h = h.symbolize_keys
|
|
140
|
+
@latitude = h[:latitude].to_f || h[:lat].to_f || @latitude
|
|
141
|
+
@longitude = h[:longitude].to_f || h[:lng].to_f || @longitude
|
|
142
|
+
elsif h.is_a?(Array) && h.count == 2
|
|
143
|
+
@latitude = h.first.to_f
|
|
144
|
+
@longitude = h.last.to_f
|
|
145
|
+
end
|
|
146
|
+
_validate_point
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @return [Boolean] true if two geopoints are equal based on lat and lng.
|
|
150
|
+
def ==(g)
|
|
151
|
+
return false unless g.is_a?(GeoPoint)
|
|
152
|
+
@latitude == g.latitude && @longitude == g.longitude
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Helper method for reducing the precision of a geopoint.
|
|
156
|
+
# @param precision [Integer] The number of floating digits to keep.
|
|
157
|
+
# @return [GeoPoint] Reduces the precision of a geopoint.
|
|
158
|
+
def estimated(precision = 2)
|
|
159
|
+
Parse::GeoPoint.new(@latitude.to_f.round(precision), @longitude.round(precision))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns a tuple containing latitude and longitude
|
|
163
|
+
# @return [Array]
|
|
164
|
+
def to_a
|
|
165
|
+
[@latitude, @longitude]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# GeoJSON (RFC 7946) representation of this point. GeoJSON requires
|
|
169
|
+
# `[longitude, latitude]` axis order — the inverse of Parse's wire
|
|
170
|
+
# format — so this method performs the swap. Useful when handing the
|
|
171
|
+
# value to Leaflet/Mapbox/PostGIS, or when constructing literals for
|
|
172
|
+
# MongoDB-direct geo queries (which use GeoJSON internally).
|
|
173
|
+
# @example
|
|
174
|
+
# geopoint.to_geojson
|
|
175
|
+
# # => {"type" => "Point", "coordinates" => [-117.6542, 32.8233]}
|
|
176
|
+
# @return [Hash] a GeoJSON `Point` geometry object.
|
|
177
|
+
def to_geojson
|
|
178
|
+
{ "type" => "Point", "coordinates" => [@longitude, @latitude] }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Build a {Parse::GeoPoint} from a GeoJSON `Point` geometry object.
|
|
182
|
+
# Accepts either symbol or string keys and the standard
|
|
183
|
+
# `[longitude, latitude]` axis order; performs the swap to Parse's
|
|
184
|
+
# `[latitude, longitude]` internal storage.
|
|
185
|
+
# @example
|
|
186
|
+
# Parse::GeoPoint.from_geojson("type" => "Point", "coordinates" => [-117.6542, 32.8233])
|
|
187
|
+
# @param geojson [Hash] a GeoJSON Point geometry object.
|
|
188
|
+
# @return [Parse::GeoPoint]
|
|
189
|
+
# @raise [ArgumentError] if the input is not a valid GeoJSON Point.
|
|
190
|
+
def self.from_geojson(geojson)
|
|
191
|
+
raise ArgumentError, "[Parse::GeoPoint] from_geojson expects a Hash." unless geojson.is_a?(Hash)
|
|
192
|
+
hash = geojson.respond_to?(:symbolize_keys) ? geojson.symbolize_keys : geojson
|
|
193
|
+
type = hash[:type] || hash["type"]
|
|
194
|
+
coords = hash[:coordinates] || hash["coordinates"]
|
|
195
|
+
# Mirror Parse::Polygon.from_geojson: require both coordinates to
|
|
196
|
+
# be finite Numerics. Without this check, non-numeric entries
|
|
197
|
+
# (`"evil"`, `{"$where": "..."}`, `nil`) silently coerce to 0.0
|
|
198
|
+
# via `.to_f`, producing a null-island point that matches
|
|
199
|
+
# ACL-relevant proximity queries unintentionally. NaN / Infinity
|
|
200
|
+
# similarly produce silent geo bugs and 2dsphere index errors.
|
|
201
|
+
unless type.to_s == "Point" && coords.is_a?(Array) && coords.length == 2 &&
|
|
202
|
+
coords[0].is_a?(Numeric) && coords[1].is_a?(Numeric) &&
|
|
203
|
+
coords[0].finite? && coords[1].finite?
|
|
204
|
+
raise ArgumentError, "[Parse::GeoPoint] from_geojson expects a GeoJSON Point with " \
|
|
205
|
+
"two finite numeric coordinates."
|
|
206
|
+
end
|
|
207
|
+
Parse::GeoPoint.new(coords[1].to_f, coords[0].to_f)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @!visibility private
|
|
211
|
+
def inspect
|
|
212
|
+
"#<GeoPoint [#{@latitude},#{@longitude}]>"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Calculate the distance in miles to another GeoPoint using Haversine.
|
|
216
|
+
# You may also call this method with a latitude and longitude.
|
|
217
|
+
# @example
|
|
218
|
+
# point.distance_in_miles(geotpoint)
|
|
219
|
+
# point.distance_in_miles(lat, lng)
|
|
220
|
+
#
|
|
221
|
+
# @param geopoint [GeoPoint]
|
|
222
|
+
# @param lng [Float] Longitude assuming that the first parameter
|
|
223
|
+
# is longitude instead of a GeoPoint.
|
|
224
|
+
# @return [Float] number of miles between geopoints.
|
|
225
|
+
# @see #distance_in_km
|
|
226
|
+
def distance_in_miles(geopoint, lng = nil)
|
|
227
|
+
distance_in_km(geopoint, lng) * 0.621371
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Calculate the distance in kilometers to another GeoPoint using Haversine
|
|
231
|
+
# method. You may also call this method with a latitude and longitude.
|
|
232
|
+
# @example
|
|
233
|
+
# point.distance_in_km(geotpoint)
|
|
234
|
+
# point.distance_in_km(lat, lng)
|
|
235
|
+
#
|
|
236
|
+
# @param geopoint [GeoPoint]
|
|
237
|
+
# @param lng [Float] Longitude assuming that the first parameter is a latitude instead of a GeoPoint.
|
|
238
|
+
# @return [Float] number of miles between geopoints.
|
|
239
|
+
# @see #distance_in_miles
|
|
240
|
+
def distance_in_km(geopoint, lng = nil)
|
|
241
|
+
unless geopoint.is_a?(Parse::GeoPoint)
|
|
242
|
+
geopoint = Parse::GeoPoint.new(geopoint, lng)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
dtor = Math::PI / 180
|
|
246
|
+
r = 6378.14
|
|
247
|
+
r_lat1 = self.latitude * dtor
|
|
248
|
+
r_lng1 = self.longitude * dtor
|
|
249
|
+
r_lat2 = geopoint.latitude * dtor
|
|
250
|
+
r_lng2 = geopoint.longitude * dtor
|
|
251
|
+
|
|
252
|
+
delta_lat = r_lat1 - r_lat2
|
|
253
|
+
delta_lng = r_lng1 - r_lng2
|
|
254
|
+
|
|
255
|
+
a = (Math::sin(delta_lat / 2.0) ** 2).to_f + (Math::cos(r_lat1) * Math::cos(r_lat2) * (Math::sin(delta_lng / 2.0) ** 2))
|
|
256
|
+
c = 2.0 * Math::atan2(Math::sqrt(a), Math::sqrt(1.0 - a))
|
|
257
|
+
d = r * c
|
|
258
|
+
d
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|