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