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