-
-
Notifications
You must be signed in to change notification settings - Fork 102
Enable tests to run in parallel by default #3895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 45 commits
f363428
2376022
cdec6bb
44d0c39
be84308
1ba12c7
1d0342e
6488c38
e8375db
a41ff64
ca38e62
703eba0
a8f9dcd
ddc6dbf
99aa587
c454c1c
c826fe5
16af5f2
00afff9
64e2abd
8280281
3b6fd79
6a23fdf
9dff309
0762c40
91407e5
2c5f0f0
6275cb2
69e265d
50ec604
28f6a6e
87e67e6
2f591d6
3266b9b
7e9e0c6
ce445f6
f7b0fb0
34c5150
ef55e43
14fa415
8e79172
f8f942c
867ccca
f58e1ad
b09a93f
136f775
05758f2
24af8f3
2ea2ee0
3b6117c
7705b9e
15b72ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* | ||
| * Copyright © 2021-present Arcade Data Ltd ([email protected]) | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected]) | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package com.arcadedb.function.geo; | ||
|
|
||
| import com.arcadedb.exception.CommandExecutionException; | ||
| import com.arcadedb.function.StatelessFunction; | ||
| import com.arcadedb.query.sql.executor.CommandContext; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Cypher {@code point.distance(point1, point2)} function. | ||
| * | ||
| * <p>Computes the distance between two points. Uses Haversine formula for WGS-84 | ||
| * geographic points (result in meters), and Euclidean distance for Cartesian points.</p> | ||
| */ | ||
| public class CypherPointDistanceFunction implements StatelessFunction { | ||
| private static final double EARTH_RADIUS_M = 6371000.0; | ||
|
|
||
| @Override | ||
| public String getName() { | ||
| return "point.distance"; | ||
| } | ||
|
|
||
| @Override | ||
| public Object execute(final Object[] args, final CommandContext context) { | ||
| if (args == null || args.length != 2) | ||
| throw new CommandExecutionException("point.distance() requires exactly 2 arguments"); | ||
| if (args[0] == null || args[1] == null) | ||
| return null; | ||
| if (!(args[0] instanceof Map) || !(args[1] instanceof Map)) | ||
| throw new CommandExecutionException("point.distance() arguments must be point values (maps)"); | ||
| final Map<?, ?> p1 = (Map<?, ?>) args[0]; | ||
| final Map<?, ?> p2 = (Map<?, ?>) args[1]; | ||
|
|
||
| // WGS-84: use Haversine formula | ||
| if (p1.containsKey("longitude") && p1.containsKey("latitude") && | ||
| p2.containsKey("longitude") && p2.containsKey("latitude")) | ||
| return haversineDistance( | ||
| ((Number) p1.get("latitude")).doubleValue(), ((Number) p1.get("longitude")).doubleValue(), | ||
| ((Number) p2.get("latitude")).doubleValue(), ((Number) p2.get("longitude")).doubleValue()); | ||
|
|
||
| // Cartesian: use Euclidean distance | ||
| final Number x1n = (Number) p1.get("x"); | ||
| final Number y1n = (Number) p1.get("y"); | ||
| final Number x2n = (Number) p2.get("x"); | ||
| final Number y2n = (Number) p2.get("y"); | ||
| if (x1n == null || y1n == null || x2n == null || y2n == null) | ||
| return null; | ||
| final double dx = x2n.doubleValue() - x1n.doubleValue(); | ||
| final double dy = y2n.doubleValue() - y1n.doubleValue(); | ||
| double sumSq = dx * dx + dy * dy; | ||
| final Number z1n = (Number) p1.get("z"); | ||
| final Number z2n = (Number) p2.get("z"); | ||
| if (z1n != null && z2n != null) { | ||
| final double dz = z2n.doubleValue() - z1n.doubleValue(); | ||
| sumSq += dz * dz; | ||
| } | ||
| return Math.sqrt(sumSq); | ||
| } | ||
|
|
||
| private double haversineDistance(final double lat1, final double lon1, final double lat2, final double lon2) { | ||
| final double dLat = Math.toRadians(lat2 - lat1); | ||
| final double dLon = Math.toRadians(lon2 - lon1); | ||
| final double a = Math.pow(Math.sin(dLat / 2), 2) | ||
| + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) | ||
| * Math.pow(Math.sin(dLon / 2), 2); | ||
| return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * EARTH_RADIUS_M; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,19 +20,23 @@ | |||||||||
|
|
||||||||||
| import com.arcadedb.exception.CommandExecutionException; | ||||||||||
| import com.arcadedb.function.StatelessFunction; | ||||||||||
| import com.arcadedb.function.sql.geo.GeoUtils; | ||||||||||
| import com.arcadedb.function.sql.geo.LightweightPoint; | ||||||||||
| import com.arcadedb.query.sql.executor.CommandContext; | ||||||||||
|
|
||||||||||
| import java.util.LinkedHashMap; | ||||||||||
| import java.util.Map; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Cypher {@code point(lat, lon)} function. | ||||||||||
| * | ||||||||||
| * <p>Constructs a spatial point from latitude and longitude. Following Cypher/Neo4j convention, | ||||||||||
| * the first argument is latitude and the second is longitude. The point is stored internally | ||||||||||
| * using the spatial4j convention (x=longitude, y=latitude) so that spatial distance functions | ||||||||||
| * such as {@code geo.distance} operate correctly.</p> | ||||||||||
| * Cypher {@code point(map)} function. | ||||||||||
| * | ||||||||||
| * <p>Usage: {@code point(<latitude>, <longitude>)}</p> | ||||||||||
| * <p>Constructs a point from a map of coordinate properties. Supports:</p> | ||||||||||
| * <ul> | ||||||||||
| * <li>WGS-84 2D: {@code point({longitude: x, latitude: y})}</li> | ||||||||||
| * <li>WGS-84 3D: {@code point({longitude: x, latitude: y, height: z})}</li> | ||||||||||
| * <li>Cartesian 2D: {@code point({x: a, y: b})}</li> | ||||||||||
| * <li>Cartesian 3D: {@code point({x: a, y: b, z: c})}</li> | ||||||||||
| * </ul> | ||||||||||
| * <p>The returned map contains the coordinate keys and a {@code crs} field indicating | ||||||||||
| * the coordinate reference system.</p> | ||||||||||
| */ | ||||||||||
| public class CypherPointFunction implements StatelessFunction { | ||||||||||
| @Override | ||||||||||
|
|
@@ -42,13 +46,84 @@ public String getName() { | |||||||||
|
|
||||||||||
| @Override | ||||||||||
| public Object execute(final Object[] args, final CommandContext context) { | ||||||||||
| if (args == null || args.length < 2) | ||||||||||
| throw new CommandExecutionException("point() requires latitude and longitude as parameters"); | ||||||||||
| if (args[0] == null || args[1] == null) | ||||||||||
| if (args == null || args.length == 0 || args.length > 2) | ||||||||||
| throw new CommandExecutionException("point() requires either one map argument (point({...})) or two numeric arguments (point(latitude, longitude))"); | ||||||||||
|
|
||||||||||
| // 2-arg positional form: point(latitude, longitude) → WGS-84 2D | ||||||||||
| if (args.length == 2) { | ||||||||||
| if (args[0] == null || args[1] == null) | ||||||||||
| return null; | ||||||||||
| if (!(args[0] instanceof Number) || !(args[1] instanceof Number)) | ||||||||||
| throw new CommandExecutionException("point() with two arguments requires numeric latitude and longitude"); | ||||||||||
| final double lat = ((Number) args[0]).doubleValue(); | ||||||||||
| final double lon = ((Number) args[1]).doubleValue(); | ||||||||||
|
Comment on lines
+58
to
+59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change reduces robustness compared to the previous implementation. The previous code used
Suggested change
|
||||||||||
| final Map<String, Object> result = new LinkedHashMap<>(); | ||||||||||
| result.put("latitude", lat); | ||||||||||
| result.put("longitude", lon); | ||||||||||
| result.put("x", lon); | ||||||||||
| result.put("y", lat); | ||||||||||
| result.put("crs", "WGS-84"); | ||||||||||
| result.put("srid", 4326); | ||||||||||
| return result; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (args[0] == null) | ||||||||||
| return null; | ||||||||||
| final double lat = GeoUtils.getDoubleValue(args[0]); | ||||||||||
| final double lon = GeoUtils.getDoubleValue(args[1]); | ||||||||||
| // Store as LightweightPoint(x=longitude, y=latitude) per spatial4j convention | ||||||||||
| return new LightweightPoint(lon, lat); | ||||||||||
| if (!(args[0] instanceof Map)) | ||||||||||
| throw new CommandExecutionException("point() argument must be a map with coordinate properties"); | ||||||||||
| final Map<?, ?> map = (Map<?, ?>) args[0]; | ||||||||||
|
|
||||||||||
| final Map<String, Object> result = new LinkedHashMap<>(); | ||||||||||
|
|
||||||||||
| if (map.containsKey("longitude") || map.containsKey("latitude")) { | ||||||||||
| // WGS-84 coordinate system | ||||||||||
| final Object lon = map.get("longitude"); | ||||||||||
| final Object lat = map.get("latitude"); | ||||||||||
| if (lon == null || lat == null) | ||||||||||
| return null; | ||||||||||
| final double x = ((Number) lon).doubleValue(); | ||||||||||
| final double y = ((Number) lat).doubleValue(); | ||||||||||
| result.put("longitude", x); | ||||||||||
| result.put("latitude", y); | ||||||||||
| result.put("x", x); | ||||||||||
| result.put("y", y); | ||||||||||
| addOptionalZ(result, map); | ||||||||||
| result.put("crs", result.containsKey("z") ? "WGS-84-3D" : "WGS-84"); | ||||||||||
| result.put("srid", result.containsKey("z") ? 4979 : 4326); | ||||||||||
| } else if (map.containsKey("x") || map.containsKey("y")) { | ||||||||||
| // Cartesian coordinate system | ||||||||||
| final Object xv = map.get("x"); | ||||||||||
| final Object yv = map.get("y"); | ||||||||||
| if (xv == null || yv == null) | ||||||||||
| return null; | ||||||||||
| final double x = ((Number) xv).doubleValue(); | ||||||||||
| final double y = ((Number) yv).doubleValue(); | ||||||||||
| result.put("x", x); | ||||||||||
| result.put("y", y); | ||||||||||
| addOptionalZ(result, map); | ||||||||||
| final Object crsObj = map.get("crs"); | ||||||||||
| if (crsObj != null) | ||||||||||
| result.put("crs", crsObj.toString()); | ||||||||||
| else | ||||||||||
| result.put("crs", result.containsKey("z") ? "cartesian-3D" : "cartesian"); | ||||||||||
| if (map.containsKey("srid")) | ||||||||||
| result.put("srid", ((Number) map.get("srid")).intValue()); | ||||||||||
| } else { | ||||||||||
| throw new CommandExecutionException("point() map must contain x/y or longitude/latitude properties"); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return result; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private void addOptionalZ(final Map<String, Object> result, final Map<?, ?> map) { | ||||||||||
| if (map.containsKey("z")) { | ||||||||||
| final Object zv = map.get("z"); | ||||||||||
| if (zv != null) | ||||||||||
| result.put("z", ((Number) zv).doubleValue()); | ||||||||||
| } else if (map.containsKey("height")) { | ||||||||||
| final Object hv = map.get("height"); | ||||||||||
| if (hv != null) | ||||||||||
| result.put("z", ((Number) hv).doubleValue()); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is unsafe as it assumes the map values are non-null and of type
Number. If a coordinate isnullin the map (which is possible in Cypher), this will throw aNullPointerException. If it's aString, it will throw aClassCastException. It is safer to useGeoUtils.getDoubleValue()after verifying the values are not null.