• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.cellbroadcastservice;
18 
19 import android.annotation.NonNull;
20 import android.telephony.CbGeoUtils.Circle;
21 import android.telephony.CbGeoUtils.Geometry;
22 import android.telephony.CbGeoUtils.LatLng;
23 import android.telephony.CbGeoUtils.Polygon;
24 import android.text.TextUtils;
25 import android.util.Log;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Objects;
32 import java.util.Optional;
33 import java.util.stream.Collectors;
34 
35 /**
36  * This utils class is specifically used for geo-targeting of CellBroadcast messages.
37  * The coordinates used by this utils class are latitude and longitude, but some algorithms in this
38  * class only use them as coordinates on plane, so the calculation will be inaccurate. So don't use
39  * this class for anything other then geo-targeting of cellbroadcast messages.
40  */
41 public class CbGeoUtils {
42     /**
43      * Tolerance for determining if the value is 0. If the absolute value of a value is less than
44      * this tolerance, it will be treated as 0.
45      */
46     public static final double EPS = 1e-7;
47 
48     private static final String TAG = "CbGeoUtils";
49 
50     /** The TLV tags of WAC, defined in ATIS-0700041 5.2.3 WAC tag coding. */
51     public static final int GEO_FENCING_MAXIMUM_WAIT_TIME = 0x01;
52     public static final int GEOMETRY_TYPE_POLYGON = 0x02;
53     public static final int GEOMETRY_TYPE_CIRCLE = 0x03;
54 
55     /** The identifier of geometry in the encoded string. */
56     private static final String CIRCLE_SYMBOL = "circle";
57     private static final String POLYGON_SYMBOL = "polygon";
58 
59     /**
60      * Parse the geometries from the encoded string {@code str}. The string must follow the
61      * geometry encoding specified by {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}.
62      */
63     @NonNull
parseGeometriesFromString(@onNull String str)64     public static List<Geometry> parseGeometriesFromString(@NonNull String str) {
65         List<Geometry> geometries = new ArrayList<>();
66         for (String geometryStr : str.split("\\s*;\\s*")) {
67             String[] geoParameters = geometryStr.split("\\s*\\|\\s*");
68             switch (geoParameters[0]) {
69                 case CIRCLE_SYMBOL:
70                     geometries.add(new Circle(parseLatLngFromString(geoParameters[1]),
71                             Double.parseDouble(geoParameters[2])));
72                     break;
73                 case POLYGON_SYMBOL:
74                     List<LatLng> vertices = new ArrayList<>(geoParameters.length - 1);
75                     for (int i = 1; i < geoParameters.length; i++) {
76                         vertices.add(parseLatLngFromString(geoParameters[i]));
77                     }
78                     geometries.add(new Polygon(vertices));
79                     break;
80                 default:
81                     final String errorMessage = "Invalid geometry format " + geometryStr;
82                     Log.e(TAG, errorMessage);
83                     CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
84                             CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_GEOMETRY_FROM_FWK,
85                             errorMessage);
86             }
87         }
88         return geometries;
89     }
90 
91     /**
92      * Encode a list of geometry objects to string. The encoding format is specified by
93      * {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}.
94      *
95      * @param geometries the list of geometry objects need to be encoded.
96      * @return the encoded string.
97      */
98     @NonNull
encodeGeometriesToString(@onNull List<Geometry> geometries)99     public static String encodeGeometriesToString(@NonNull List<Geometry> geometries) {
100         return geometries.stream()
101                 .map(geometry -> encodeGeometryToString(geometry))
102                 .filter(encodedStr -> !TextUtils.isEmpty(encodedStr))
103                 .collect(Collectors.joining(";"));
104     }
105 
106 
107     /**
108      * Encode the geometry object to string. The encoding format is specified by
109      * {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}.
110      * @param geometry the geometry object need to be encoded.
111      * @return the encoded string.
112      */
113     @NonNull
encodeGeometryToString(@onNull Geometry geometry)114     private static String encodeGeometryToString(@NonNull Geometry geometry) {
115         StringBuilder sb = new StringBuilder();
116         if (geometry instanceof Polygon) {
117             sb.append(POLYGON_SYMBOL);
118             for (LatLng latLng : ((Polygon) geometry).getVertices()) {
119                 sb.append("|");
120                 sb.append(latLng.lat);
121                 sb.append(",");
122                 sb.append(latLng.lng);
123             }
124         } else if (geometry instanceof Circle) {
125             sb.append(CIRCLE_SYMBOL);
126             Circle circle = (Circle) geometry;
127 
128             // Center
129             sb.append("|");
130             sb.append(circle.getCenter().lat);
131             sb.append(",");
132             sb.append(circle.getCenter().lng);
133 
134             // Radius
135             sb.append("|");
136             sb.append(circle.getRadius());
137         } else {
138             Log.e(TAG, "Unsupported geometry object " + geometry);
139             return null;
140         }
141         return sb.toString();
142     }
143 
144     /**
145      * Parse {@link LatLng} from {@link String}. Latitude and longitude are separated by ",".
146      * Example: "13.56,-55.447".
147      *
148      * @param str encoded lat/lng string.
149      * @Return {@link LatLng} object.
150      */
151     @NonNull
parseLatLngFromString(@onNull String str)152     private static LatLng parseLatLngFromString(@NonNull String str) {
153         String[] latLng = str.split("\\s*,\\s*");
154         return new LatLng(Double.parseDouble(latLng[0]), Double.parseDouble(latLng[1]));
155     }
156 
157     private static final double SCALE = 1000.0 * 100.0;
158 
159 
160     /**
161      * Computes the shortest distance of {@code geo} to {@code latLng}.  If {@code geo} does not
162      * support this functionality, {@code Optional.empty()} is returned.
163      *
164      * @hide
165      * @param geo shape
166      * @param latLng point to calculate against
167      * @return the distance in meters
168      */
169     @VisibleForTesting
distance(Geometry geo, @NonNull LatLng latLng)170     public static Optional<Double> distance(Geometry geo,
171             @NonNull LatLng latLng) {
172         if (geo instanceof android.telephony.CbGeoUtils.Polygon) {
173             CbGeoUtils.DistancePolygon distancePolygon =
174                     new CbGeoUtils.DistancePolygon((Polygon) geo);
175             return Optional.of(distancePolygon.distance(latLng));
176         } else if (geo instanceof android.telephony.CbGeoUtils.Circle) {
177             CbGeoUtils.DistanceCircle distanceCircle =
178                     new CbGeoUtils.DistanceCircle((Circle) geo);
179             return Optional.of(distanceCircle.distance(latLng));
180         } else {
181             return Optional.empty();
182         }
183     }
184 
185     /**
186      * Will be merged with {@code CbGeoUtils.Circle} in future release.
187      *
188      * @hide
189      */
190     @VisibleForTesting
191     public static class DistanceCircle {
192         private final Circle mCircle;
193 
DistanceCircle(Circle circle)194         DistanceCircle(Circle circle) {
195             mCircle = circle;
196         }
197 
198         /**
199          * Distance in meters.  If you are within the bounds of the circle, returns a
200          * negative distance to the edge.
201          * @param latLng the coordinate to calculate distance against
202          * @return the distance given in meters
203          */
distance(@onNull final LatLng latLng)204         public double distance(@NonNull final LatLng latLng) {
205             return latLng.distance(mCircle.getCenter()) - mCircle.getRadius();
206         }
207     }
208     /**
209      * Will be merged with {@code CbGeoUtils.Polygon} in future release.
210      *
211      * @hide
212      */
213     @VisibleForTesting
214     public static class DistancePolygon {
215 
216         @NonNull private final Polygon mPolygon;
217         @NonNull private final LatLng mOrigin;
218 
DistancePolygon(@onNull final Polygon polygon)219         public DistancePolygon(@NonNull final Polygon polygon) {
220             mPolygon = polygon;
221 
222             // Find the point with smallest longitude as the mOrigin point.
223             int idx = 0;
224             for (int i = 1; i < polygon.getVertices().size(); i++) {
225                 if (polygon.getVertices().get(i).lng < polygon.getVertices().get(idx).lng) {
226                     idx = i;
227                 }
228             }
229             mOrigin = polygon.getVertices().get(idx);
230         }
231 
232         /**
233          * Returns the meters difference between {@code latLng} to the closest point in the polygon.
234          *
235          * Note: The distance given becomes less accurate as you move further north and south.
236          *
237          * @param latLng the coordinate to calculate distance against
238          * @return the distance given in meters
239          */
distance(@onNull final LatLng latLng)240         public double distance(@NonNull final LatLng latLng) {
241             double minDistance = Double.MAX_VALUE;
242             List<LatLng> vertices = mPolygon.getVertices();
243             int n = mPolygon.getVertices().size();
244             for (int i = 0; i < n; i++) {
245                 LatLng a = vertices.get(i);
246                 LatLng b = vertices.get((i + 1) % n);
247 
248                 // The converted points are distances (in meters) to the origin point.
249                 // see: #convertToDistanceFromOrigin
250                 Point sa = convertToDistanceFromOrigin(a);
251                 Point sb = convertToDistanceFromOrigin(b);
252                 Point sp = convertToDistanceFromOrigin(latLng);
253 
254                 CbGeoUtils.LineSegment l = new CbGeoUtils.LineSegment(sa, sb);
255                 double d = l.distance(sp);
256                 minDistance = Math.min(d, minDistance);
257             }
258             return minDistance;
259         }
260 
261         /**
262          * Move the given point {@code latLng} to the coordinate system with {@code mOrigin} as the
263          * origin. {@code mOrigin} is selected from the vertices of a polygon, it has
264          * the smallest longitude value among all of the polygon vertices.  The unit distance
265          * between points is meters.
266          *
267          * @param latLng the point need to be converted and scaled.
268          * @Return a {@link Point} object
269          */
convertToDistanceFromOrigin(LatLng latLng)270         private Point convertToDistanceFromOrigin(LatLng latLng) {
271             return CbGeoUtils.convertToDistanceFromOrigin(mOrigin, latLng);
272         }
273     }
274 
275     /**
276      * We calculate the new point by finding the distances between the latitude and longitude
277      * components independently from {@code latLng} to the {@code origin}.
278      *
279      * This ends up giving us a {@code Point} such that:
280      * {@code x = distance(latLng.lat, origin.lat)}
281      * {@code y = distance(latLng.lng, origin.lng)}
282      *
283      * This allows us to use simple formulas designed for a cartesian coordinate system when
284      * calculating the distance from a point to a line segment.
285      *
286      * @param origin the origin lat lng in which to convert and scale {@code latLng}
287      * @param latLng the lat lng need to be converted and scaled.
288      * @return a {@link Point} object.
289      *
290      * @hide
291      */
292     @VisibleForTesting
convertToDistanceFromOrigin(@onNull final LatLng origin, @NonNull final LatLng latLng)293     public static Point convertToDistanceFromOrigin(@NonNull final LatLng origin,
294             @NonNull final LatLng latLng) {
295         double x = new LatLng(latLng.lat, origin.lng).distance(new LatLng(origin.lat, origin.lng));
296         double y = new LatLng(origin.lat, latLng.lng).distance(new LatLng(origin.lat, origin.lng));
297 
298         x = latLng.lat > origin.lat ? x : -x;
299         y = latLng.lng > origin.lng ? y : -y;
300         return new Point(x, y);
301     }
302 
303     /**
304      * @hide
305      */
306     @VisibleForTesting
307     public static class Point {
308         /**
309          * x-coordinate
310          */
311         public final double x;
312 
313         /**
314          * y-coordinate
315          */
316         public final double y;
317 
318         /**
319          * ..ctor
320          * @param x
321          * @param y
322          */
Point(double x, double y)323         public Point(double x, double y) {
324             this.x = x;
325             this.y = y;
326         }
327 
328         /**
329          * Subtracts the two points
330          * @param p
331          * @return
332          */
subtract(Point p)333         public Point subtract(Point p) {
334             return new Point(x - p.x, y - p.y);
335         }
336 
337         /**
338          * Calculates the distance between the two points
339          * @param pt
340          * @return
341          */
distance(Point pt)342         public double distance(Point pt) {
343             return Math.sqrt(Math.pow(x - pt.x, 2) + Math.pow(y - pt.y, 2));
344         }
345 
346         @Override
equals(Object o)347         public boolean equals(Object o) {
348             if (this == o) return true;
349             if (o == null || getClass() != o.getClass()) return false;
350             Point point = (Point) o;
351             return Double.compare(point.x, x) == 0
352                     && Double.compare(point.y, y) == 0;
353         }
354 
355         @Override
hashCode()356         public int hashCode() {
357             return Objects.hash(x, y);
358         }
359 
360         @Override
toString()361         public String toString() {
362             return "(" + x + ", " + y + ")";
363         }
364     }
365 
366     /**
367      * Represents a line segment. This is used for handling geo-fenced cell broadcasts.
368      * More information regarding cell broadcast geo-fencing logic is
369      * laid out in 3GPP TS 23.041 and ATIS-0700041.
370      */
371     @VisibleForTesting
372     public static final class LineSegment {
373         @NonNull final Point mPtA;
374         @NonNull final Point mPtB;
375 
LineSegment(@onNull final Point ptA, @NonNull final Point ptB)376         public LineSegment(@NonNull final Point ptA, @NonNull final Point ptB) {
377             this.mPtA = ptA;
378             this.mPtB = ptB;
379         }
380 
getLength()381         public double getLength() {
382             return this.mPtA.distance(this.mPtB);
383         }
384 
385         /**
386          * Calculates the closest distance from {@code pt} to this line segment.
387          *
388          * @param pt the point to calculate against
389          * @return the distance in meters
390          */
distance(Point pt)391         public double distance(Point pt) {
392             final double lengthSquared = getLength() * getLength();
393             if (lengthSquared == 0.0) {
394                 return pt.distance(this.mPtA);
395             }
396 
397             Point sub1 = pt.subtract(mPtA);
398             Point sub2 = mPtB.subtract(mPtA);
399             double dot = sub1.x * sub2.x + sub1.y * sub2.y;
400 
401             //Magnitude of projection
402             double magnitude = dot / lengthSquared;
403 
404             //Keep bounded between 0.0 and 1.0
405             if (magnitude > 1.0) {
406                 magnitude = 1.0;
407             } else if (magnitude < 0.0) {
408                 magnitude = 0.0;
409             }
410 
411             final double projX = calcProjCoordinate(this.mPtA.x, this.mPtB.x, magnitude);
412             final double projY = calcProjCoordinate(this.mPtA.y, this.mPtB.y, magnitude);
413 
414             final Point proj = new Point(projX, projY);
415             return proj.distance(pt);
416         }
417 
calcProjCoordinate(double aVal, double bVal, double m)418         private static double calcProjCoordinate(double aVal, double bVal, double m) {
419             return aVal + ((bVal - aVal) * m);
420         }
421     }
422 }
423