-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathPoint.java
More file actions
417 lines (363 loc) · 15.7 KB
/
Point.java
File metadata and controls
417 lines (363 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
/*
* Copyright (C) 2016-2021, Stichting Mapcode Foundation (http://www.mapcode.com)
*
* 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.
*/
package com.mapcode;
import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.Random;
import static com.mapcode.CheckArgs.checkNonnull;
/**
* This class defines a class for lat/lon points.
*
* Internally, the class implements a fixed-point representation where a coordinate is expressed in
* "fractions", of 1/3.240,000,000,000th of a degree. A double (an IEEE 754-1985 binary64) is just
* sufficient to represent coordinates between -180 and +180 degrees in such fractions.
* However, for applications that use micro-degrees a lot, the implementation below is more efficient.
* It represent the fractions in pairs of integers, the first integer
* representing 1/1,000,000th of degrees, the second representing the remainder.
*/
@SuppressWarnings("MagicNumber")
public final class Point {
// Latitude and longitude ranges.
@SuppressWarnings("unused")
public static final double LON_DEG_MIN = -180.0;
@SuppressWarnings("unused")
public static final double LON_DEG_MAX = 180.0;
@SuppressWarnings("unused")
public static final double LAT_DEG_MIN = -90.0;
@SuppressWarnings("unused")
public static final double LAT_DEG_MAX = 90.0;
// Conversion constants.
public static final int DEG_TO_MICRO_DEG = 1000000;
public static final int MICRO_DEG_90 = 90 * DEG_TO_MICRO_DEG;
public static final int MICRO_DEG_180 = 180 * DEG_TO_MICRO_DEG;
public static final int MICRO_DEG_360 = 360 * DEG_TO_MICRO_DEG;
// Radius of Earth.
public static final double EARTH_RADIUS_X_METERS = 6378137.0;
public static final double EARTH_RADIUS_Y_METERS = 6356752.3;
// Circumference of Earth.
public static final double EARTH_CIRCUMFERENCE_X = EARTH_RADIUS_X_METERS * 2.0 * Math.PI;
public static final double EARTH_CIRCUMFERENCE_Y = EARTH_RADIUS_Y_METERS * 2.0 * Math.PI;
// Meters per degree latitude is fixed. For longitude: use factor * cos(midpoint of two degree latitudes).
public static final double METERS_PER_DEGREE_LAT = EARTH_CIRCUMFERENCE_Y / 360.0;
public static final double METERS_PER_DEGREE_LON_EQUATOR = EARTH_CIRCUMFERENCE_X / 360.0; // * cos(deg(lat)).
/**
* Create a point from lat/lon in degrees (may be precision!)
*
* @param latDeg Latitude in degrees. Range: [-90, 90].
* @param lonDeg Longitude in degrees. Range: [-180, 180).
* @return A defined point.
*/
@Nonnull
public static Point fromDeg(final double latDeg, final double lonDeg) {
return new Point(latDeg, lonDeg);
}
/**
* Public construction, from integer microdegrees (no loss of precision).
*
* @param latMicroDeg Latitude, in microdegrees.
* @param lonMicroDeg Longitude, in microdegrees.
* @return A defined point.
*/
@Nonnull
public static Point fromMicroDeg(final int latMicroDeg, final int lonMicroDeg) {
final Point p = new Point();
p.latMicroDeg = latMicroDeg;
p.latFractionOnlyDeg = 0;
p.lonMicroDeg = lonMicroDeg;
p.lonFractionOnlyDeg = 0;
p.defined = true;
return p.wrap();
}
/**
* Get the latitude in degrees (may lose precision).
*
* @return Latitude in degrees. No range is enforced.
*/
public double getLatDeg() {
assert defined;
return (latMicroDeg / MICRODEG_TO_DEG_FACTOR) + (latFractionOnlyDeg / LAT_TO_FRACTIONS_FACTOR);
}
/**
* Get the longitude in degrees (may lose precision).
*
* @return Longitude in degrees. No range is enforced.
*/
public double getLonDeg() {
assert defined;
return (lonMicroDeg / MICRODEG_TO_DEG_FACTOR) + (lonFractionOnlyDeg / LON_TO_FRACTIONS_FACTOR);
}
/**
* Get latitude as micro-degrees. Note that this looses precision beyond microdegrees!
*
* @return floor(Latitude in microdegrees)
*/
public int getLatMicroDeg() {
assert defined;
return latMicroDeg;
}
/**
* Get longitude as micro-degrees. Note that this looses precision beyond microdegrees!
*
* @return floor(Longitude in microdegrees)
*/
public int getLonMicroDeg() {
assert defined;
return lonMicroDeg;
}
/**
* Create a random point, uniformly distributed over the surface of the Earth.
*
* @param randomGenerator Random generator used to create a point.
* @return Random point with uniform distribution over the sphere.
*/
@Nonnull
public static Point fromUniformlyDistributedRandomPoints(@Nonnull final Random randomGenerator) {
checkNonnull("randomGenerator", randomGenerator);
// Calculate uniformly distributed 3D point on sphere (radius = 1.0):
// http://mathproofs.blogspot.co.il/2005/04/uniform-random-distribution-on-sphere.html
final double unitRand1 = randomGenerator.nextDouble();
final double unitRand2 = randomGenerator.nextDouble();
final double theta0 = (2.0 * Math.PI) * unitRand1;
final double theta1 = Math.acos(1.0 - (2.0 * unitRand2));
final double x = Math.sin(theta0) * Math.sin(theta1);
final double y = Math.cos(theta0) * Math.sin(theta1);
final double z = Math.cos(theta1);
// Convert Carthesian 3D point into lat/lon (radius = 1.0):
// http://stackoverflow.com/questions/1185408/converting-from-longitude-latitude-to-cartesian-coordinates
final double latRad = Math.asin(z);
final double lonRad = Math.atan2(y, x);
// Convert radians to degrees.
assert !Double.isNaN(latRad);
assert !Double.isNaN(lonRad);
final double lat = latRad * (180.0 / Math.PI);
final double lon = lonRad * (180.0 / Math.PI);
return fromDeg(lat, lon);
}
/**
* Calculate the distance between two points. This algorithm does not take the curvature of the Earth into
* account, so it only works for small distance up to, say 200 km, and not too close to the poles.
*
* @param p1 Point 1.
* @param p2 Point 2.
* @return Straight distance between p1 and p2. Only accurate for small distances up to 200 km.
*/
public static double distanceInMeters(@Nonnull final Point p1, @Nonnull final Point p2) {
checkNonnull("p1", p1);
checkNonnull("p2", p2);
final Point from;
final Point to;
if (p1.getLonDeg() <= p2.getLonDeg()) {
from = p1;
to = p2;
} else {
from = p2;
to = p1;
}
// Calculate mid point of 2 latitudes.
final double avgLat = (from.getLatDeg() + to.getLatDeg()) / 2.0;
final double deltaLatDeg = Math.abs(to.getLatDeg() - from.getLatDeg());
final double deltaLonDeg360 = Math.abs(to.getLonDeg() - from.getLonDeg());
final double deltaLonDeg = ((deltaLonDeg360 <= 180.0) ? deltaLonDeg360 : (360.0 - deltaLonDeg360));
// Meters per longitude is fixed; per latitude requires * cos(avg(lat)).
final double deltaXMeters = degreesLonToMetersAtLat(deltaLonDeg, avgLat);
final double deltaYMeters = degreesLatToMeters(deltaLatDeg);
// Calculate length through Earth. This is an approximation, but works fine for short distances.
return Math.sqrt((deltaXMeters * deltaXMeters) + (deltaYMeters * deltaYMeters));
}
public static double degreesLatToMeters(final double latDegrees) {
return latDegrees * METERS_PER_DEGREE_LAT;
}
public static double degreesLonToMetersAtLat(final double lonDegrees, final double lat) {
return lonDegrees * METERS_PER_DEGREE_LON_EQUATOR * Math.cos(Math.toRadians(lat));
}
public static double metersToDegreesLonAtLat(final double eastMeters, final double lat) {
return (eastMeters / METERS_PER_DEGREE_LON_EQUATOR) / Math.cos(Math.toRadians(lat));
}
@Nonnull
@Override
public String toString() {
return defined ? ("(" + getLatDeg() + ", " + getLonDeg() + ')') : "undefined";
}
@SuppressWarnings("NonFinalFieldReferencedInHashCode")
@Override
public int hashCode() {
return Arrays.hashCode(new Object[]{latMicroDeg, lonMicroDeg, latFractionOnlyDeg, lonFractionOnlyDeg, defined});
}
@SuppressWarnings("NonFinalFieldReferenceInEquals")
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Point)) {
return false;
}
final Point that = (Point) obj;
return (this.latMicroDeg == that.latMicroDeg) &&
(this.lonMicroDeg == that.lonMicroDeg) &&
(this.latFractionOnlyDeg == that.latFractionOnlyDeg) &&
(this.lonFractionOnlyDeg == that.lonFractionOnlyDeg) &&
(this.defined == that.defined);
}
// -----------------------------------------------------------------------
// (Package) private data and methods.
// -----------------------------------------------------------------------
// Constants to convert between Degrees, MicroDegrees and Fractions
static final double MICRODEG_TO_DEG_FACTOR = 1000000.0;
static final double MAX_PRECISION_FACTOR = 810000.0;
static final double LAT_MICRODEG_TO_FRACTIONS_FACTOR = MAX_PRECISION_FACTOR;
static final double LON_MICRODEG_TO_FRACTIONS_FACTOR = MAX_PRECISION_FACTOR * 4;
static final double LAT_TO_FRACTIONS_FACTOR = MICRODEG_TO_DEG_FACTOR * LAT_MICRODEG_TO_FRACTIONS_FACTOR;
static final double LON_TO_FRACTIONS_FACTOR = MICRODEG_TO_DEG_FACTOR * LON_MICRODEG_TO_FRACTIONS_FACTOR;
private int latMicroDeg; // Whole nr of MICRODEG_TO_DEG_FACTOR.
private int lonMicroDeg; // Whole nr of MICRODEG_TO_DEG_FACTOR.
private int latFractionOnlyDeg; // Whole nr of LAT_TO_FRACTIONS_FACTOR, relative to latMicroDeg.
private int lonFractionOnlyDeg; // Whole nr of LON_TO_FRACTIONS_FACTOR, relative to lonMicroDeg.
/**
* Points can be "undefined" within the mapcode implementation, but never outside of that.
* Any methods creating or setting undefined points must be package private and external
* interfaces must never pass undefined points to callers.
*/
private boolean defined;
/**
* Private constructors.
*/
private Point() {
defined = false;
}
/**
* Public construction, from floating point degrees (potentially lossy).
*/
@SuppressWarnings("NumericCastThatLosesPrecision")
private Point(final double latDeg, final double lonDeg) {
double lat = latDeg + 90;
if (lat < 0) {
lat = 0;
} else if (lat > 180) {
lat = 180;
}
// Rounding factor.
final double fractionRounding = 0.1;
// Lat now [0..180].
lat = lat * LAT_TO_FRACTIONS_FACTOR;
double latFractionOnly = Math.floor(lat + fractionRounding);
latMicroDeg = (int) (latFractionOnly / LAT_MICRODEG_TO_FRACTIONS_FACTOR);
latFractionOnly = latFractionOnly - ((double) latMicroDeg * LAT_MICRODEG_TO_FRACTIONS_FACTOR);
latFractionOnlyDeg = (int) latFractionOnly;
latMicroDeg = latMicroDeg - MICRO_DEG_90;
// Math.floor has limited precision for really large values, so we need to limit the lon explicitly.
double lon = Math.min(360.0, Math.max(0.0, lonDeg - (360.0 * Math.floor(lonDeg / 360.0))));
if (Double.compare(lon, 360.0) == 0) {
lon = 0.0;
}
// Lon now in [0..360>.
lon = lon * LON_TO_FRACTIONS_FACTOR;
double lonFractionOnly = Math.floor(lon + fractionRounding);
lonMicroDeg = (int) (lonFractionOnly / LON_MICRODEG_TO_FRACTIONS_FACTOR);
lonFractionOnly = lonFractionOnly - ((double) lonMicroDeg * LON_MICRODEG_TO_FRACTIONS_FACTOR);
lonFractionOnlyDeg = (int) lonFractionOnly;
// Wrap lonMicroDeg from [0..360> to [-180..180).
if (lonMicroDeg >= MICRO_DEG_180) {
lonMicroDeg = lonMicroDeg - MICRO_DEG_360;
}
defined = true;
}
/**
* Get the the longitude "fractions", which is a whole number of 1/LON_TO_FRACTIONS_FACTOR-th
* degrees versus the millionths of degrees.
*/
int getLonFraction() {
assert defined;
return lonFractionOnlyDeg;
}
/**
* Get the the latitude "fractions", which is a whole number of 1/LAT_TO_FRACTIONS_FACTOR-th
* degrees versus the millionths of degrees
*/
int getLatFraction() {
assert defined;
return latFractionOnlyDeg;
}
/**
* Package private construction, from integer fractions (no loss of precision).
*/
@SuppressWarnings("NumericCastThatLosesPrecision")
@Nonnull
static Point fromLatLonFractions(final double latFraction, final double lonFraction) {
final Point p = new Point();
p.latMicroDeg = (int) Math.floor(latFraction / LAT_MICRODEG_TO_FRACTIONS_FACTOR);
p.latFractionOnlyDeg = (int) (latFraction - (LAT_MICRODEG_TO_FRACTIONS_FACTOR * p.latMicroDeg));
p.lonMicroDeg = (int) Math.floor(lonFraction / LON_MICRODEG_TO_FRACTIONS_FACTOR);
p.lonFractionOnlyDeg = (int) (lonFraction - (LON_MICRODEG_TO_FRACTIONS_FACTOR * p.lonMicroDeg));
p.defined = true;
return p.wrap();
}
static int degToMicroDeg(final double deg) {
//noinspection NumericCastThatLosesPrecision
return (int) Math.floor(deg * MICRODEG_TO_DEG_FACTOR);
}
static double microDegToDeg(final int microDeg) {
return ((double) microDeg) / MICRODEG_TO_DEG_FACTOR;
}
@Nonnull
Point wrap() {
if (defined) {
// Cut latitude to [-90, 90].
if (latMicroDeg < -MICRO_DEG_90) {
latMicroDeg = -MICRO_DEG_90;
latFractionOnlyDeg = 0;
}
if (latMicroDeg > MICRO_DEG_90) {
latMicroDeg = MICRO_DEG_90;
latFractionOnlyDeg = 0;
}
// Map longitude to [-180, 180). Values outside this range are wrapped to this range.
lonMicroDeg %= MICRO_DEG_360;
if (lonMicroDeg >= MICRO_DEG_180) {
lonMicroDeg -= MICRO_DEG_360;
} else if (lonMicroDeg < -MICRO_DEG_180) {
lonMicroDeg += MICRO_DEG_360;
}
}
return this;
}
/**
* Create an undefined points. No latitude or longitude can be obtained from it.
* Only within the mapcode implementation points can be undefined, so this methods is package private.
*
* @return Undefined points.
*/
@Nonnull
static Point undefined() {
return new Point();
}
/**
* Set a point to be undefined, invalidating the latitude and longitude.
* Only within the mapcode implementation points can be undefined, so this methods is package private.
*/
void setUndefined() {
defined = false;
}
/**
* Return whether the point is defined or not.
* Only within the mapcode implementation points can be undefined, so this methods is package private.
*
* @return True if defined. If false, no lat/lon is available.
*/
boolean isDefined() {
return defined;
}
}