001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014package org.gbif.common.parsers.geospatial; 015 016import java.util.HashSet; 017import java.util.List; 018import java.util.Set; 019 020import org.apache.commons.math3.util.Precision; 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024/** 025 * Utilities for dealing with CellId. 026 */ 027public class CellIdUtils { 028 029 private static final Logger LOGGER = LoggerFactory.getLogger(CellIdUtils.class); 030 031 private static final int MAX_LATITUDE = 90; 032 private static final int MIN_LATITUDE = -MAX_LATITUDE; 033 private static final int MAX_LONGITUDE = 180; 034 private static final int MIN_LONGITUDE = -MAX_LONGITUDE; 035 036 private CellIdUtils() { 037 throw new UnsupportedOperationException("Can't initialize class"); 038 } 039 040 /** 041 * Determines the cell id for the Lat / Long provided. 042 * 043 * @param latitude Which may be null 044 * @param longitude Which may be null 045 * 046 * @return The cell id for the Lat Long pair 047 * 048 * @throws UnableToGenerateCellIdException 049 * Should the lat long be null or invalid 050 */ 051 public static int toCellId(Double latitude, Double longitude) throws UnableToGenerateCellIdException { 052 LOGGER.debug("Getting cell for [{},{}]", latitude, longitude); 053 054 if (latitude == null || latitude < MIN_LATITUDE || latitude > MAX_LATITUDE || longitude < MIN_LONGITUDE 055 || longitude > MAX_LONGITUDE) { 056 throw new UnableToGenerateCellIdException( 057 "Latitude[" + latitude + "], Longitude[" + longitude + "] cannot be converted to a cell id"); 058 } else { 059 int la = getCellIdFor(latitude); 060 int lo = getMod360CellIdFor(longitude); 061 return la + lo; 062 } 063 } 064 065 /** 066 * Get mod 360 cell id. 067 */ 068 public static int getMod360CellIdFor(double longitude) { 069 return Double.valueOf(Math.floor(longitude + MAX_LONGITUDE)).intValue(); 070 } 071 072 /** 073 * Get cell id. 074 */ 075 public static int getCellIdFor(double latitude) { 076 return Double.valueOf(Math.floor(latitude + MAX_LATITUDE)).intValue() * 360; 077 } 078 079 /** 080 * Returns true if the supplied cell id lies in the bounding box demarcated by the min and max cell ids supplied. 081 */ 082 public static boolean isCellIdInBoundingBox(int cellId, int minCellId, int maxCellId) throws Exception { 083 return cellId >= minCellId && cellId <= (maxCellId - 361) && (cellId % 360) >= (minCellId % 360) 084 && (cellId % 360) <= ((maxCellId - 361) % 360); 085 } 086 087 /** 088 * Determines the centi cell id for the given values 089 * 090 * @param latitude Which may be null 091 * @param longitude Which may be null 092 * 093 * @return The centi cell id within the cell for the lat long 094 * 095 * @throws UnableToGenerateCellIdException 096 * Shoudl the lat long be null or invalid 097 */ 098 public static int toCentiCellId(Double latitude, Double longitude) throws UnableToGenerateCellIdException { 099 if (latitude == null || latitude < MIN_LATITUDE || latitude > MAX_LATITUDE || longitude < MIN_LONGITUDE 100 || longitude > MAX_LONGITUDE) { 101 throw new UnableToGenerateCellIdException( 102 "Latitude[" + latitude + "], Longitude[" + longitude + "] cannot be " + "converted to a centi cell id"); 103 } else { 104 105 //get decimal value for up to 4 decimal places 106 //17.2-> 172000 -> 2000 107 int la = Math.abs((int) (latitude * 10000) % 10000); 108 if (latitude < 0) la = 10000 - la; 109 la = (la / 1000) % 10; 110 int lo = Math.abs((int) (longitude * 10000) % 10000); 111 if (longitude < 0) lo = 10000 - lo; 112 lo = (lo / 1000) % 10; 113 114 int centiCellId = (la * 10) + lo; 115 return Math.abs(centiCellId); 116 } 117 } 118 119 /** 120 * Returns the box of the given cell This may require some more work to avoid divide rounding errors 121 * 122 * @param cellId To return the lat long box of 123 * 124 * @return The box 125 */ 126 public static LatLngBoundingBox toBoundingBox(int cellId) { 127 int longitude = (cellId % 360) - MAX_LONGITUDE; 128 int latitude = MIN_LATITUDE; 129 if (cellId > 0) { 130 latitude = Double.valueOf(Math.floor(cellId / 360)).intValue() - MAX_LATITUDE; 131 } 132 return new LatLngBoundingBox(longitude, latitude, longitude + 1, latitude + 1); 133 } 134 135 /** 136 * Returns the box of the given cell and centi cell An attempt has been made to avoid rounding errors with doubles, 137 * but may need revisited 138 * 139 * @param cellId To return the lat long box of 140 * @param centiCellId within the box 141 * 142 * @return The box 143 */ 144 public static LatLngBoundingBox toBoundingBox(int cellId, int centiCellId) { 145 int longitudeX10 = 10 * ((cellId % 360) - MAX_LONGITUDE); 146 int latitudeX10 = -900; 147 if (cellId > 0) { 148 latitudeX10 = 10 * (Double.valueOf(Math.floor(cellId / 360)).intValue() - MAX_LATITUDE); 149 } 150 151 double longOffset = (centiCellId % 10); 152 double latOffset = 0; 153 if (centiCellId > 0) { 154 latOffset = centiCellId / 10; 155 } 156 157 double minLatitude = (latitudeX10 + latOffset) / 10; 158 double minLongitude = (longitudeX10 + longOffset) / 10; 159 double maxLatitude = (latitudeX10 + latOffset + 1) / 10; 160 double maxLongitude = (longitudeX10 + longOffset + 1) / 10; 161 return new LatLngBoundingBox(minLongitude, minLatitude, maxLongitude, maxLatitude); 162 } 163 164 /** 165 * Gets the list of cells that are enclosed within the bounding box. Cells that are partially enclosed are returned 166 * also 167 * TODO implement this properly, the current version will include cells that are partially included on the bottom and 168 * left, but not the top and right 169 * 170 * @return The cells that are enclosed by the bounding box 171 * 172 * @throws UnableToGenerateCellIdException 173 * if the lat longs are invalid 174 */ 175 public static Set<Integer> getCellsEnclosedBy(double minLat, double maxLat, double minLong, double maxLong) 176 throws UnableToGenerateCellIdException { 177 if (minLat < MIN_LATITUDE) minLat = MIN_LATITUDE; 178 if (maxLat > MAX_LATITUDE) maxLat = MAX_LATITUDE; 179 if (minLong < MIN_LONGITUDE) minLong = MIN_LONGITUDE; 180 if (maxLong > MAX_LONGITUDE) maxLong = MAX_LONGITUDE; 181 182 LOGGER.debug("Establishing cells enclosed by: {}:{} {}:{}", new Object[] {minLat, maxLat, minLong, maxLong}); 183 184 int lower = toCellId(minLat, minLong); 185 int upper = toCellId(maxLat, maxLong); 186 187 LOGGER.debug("Unprocessed cells: {} -> {}", lower, upper); 188 189 // if the BB upper right corner is on a grid, then it needs flagged 190 if (Math.ceil(maxLong) == Math.floor(maxLong)) { 191 LOGGER.debug("Longitude lies on a boundary"); 192 upper -= 1; 193 } 194 if (Math.ceil(maxLat) == Math.floor(maxLat)) { 195 LOGGER.debug("Latitude lies on a boundary"); 196 upper -= 360; 197 } 198 199 LOGGER.debug("Getting cells contained in {} to {}", lower, upper); 200 201 int omitLeft = lower % 360; 202 int omitRight = upper % 360; 203 if (omitRight == 0) omitRight = 360; 204 Set<Integer> cells = new HashSet<Integer>(); 205 for (int i = lower; i <= upper; i++) { 206 if (i % 360 >= omitLeft && i % 360 <= omitRight) { 207 cells.add(i); 208 } 209 } 210 return cells; 211 } 212 213 /** 214 * Return a min cell id and a max cell id for this bounding box. 215 * 216 * @return the minCellId in int[0] and maxCellId in int[1] 217 */ 218 public static int[] getMinMaxCellIdsForBoundingBox(double minLongitude, double minLatitude, double maxLongitude, 219 double maxLatitude) throws UnableToGenerateCellIdException { 220 221 int minCellId = toCellId(minLatitude, minLongitude); 222 int maxCellId = toCellId(maxLatitude, maxLongitude); 223 224 if (Math.ceil(maxLatitude) == Math.floor(maxLatitude) && Math.ceil(maxLongitude) == Math.floor(maxLongitude) 225 && maxLongitude != 180f && maxLatitude != 90f && maxCellId > 0) { 226 227 //the maxLongitude,maxLatitude point is on a cell intersection, hence the maxCellId should be 228 // -361 the maxCellId CellIdUtils will give us i.e. the cell that is 229 // 1 below and 1 to the left of the cell id retrieved 230 //unless it is the 64799 cell. 231 maxCellId = maxCellId - 361; 232 } 233 234 return new int[] {minCellId, maxCellId}; 235 } 236 237 /** 238 * Creates a bounding box for the list of unordered cell ids. 239 * 240 * @return a LatLngBoundingBox that encapsulates this list of cell ids. 241 */ 242 public static LatLngBoundingBox getBoundingBoxForCells(List<Integer> cellIds) { 243 if (cellIds.isEmpty()) return null; 244 //first cell - gives the minLat 245 //double minLatitude = toBoundingBox(cellIds.get(0)).minLat; 246 //last cell - give the maxLat 247 //double maxLatitude = toBoundingBox(cellIds.get(cellIds.size()-1)).maxLat; 248 int minLatitudeCellId = cellIds.get(0); 249 int maxLatitudeCellId = cellIds.get(cellIds.size() - 1); 250 251 int minLongitudeCellId = cellIds.get(0); 252 int maxLongitudeCellId = cellIds.get(cellIds.size() - 1); 253 //the min cell (id % 360) - gives min longitude 254 //the max cell (id % 360) - gives max longitude 255 for (Integer cellId : cellIds) { 256 257 Integer cellIdMod360 = cellId % 360; 258 if (cellIdMod360 < (minLongitudeCellId % 360)) minLongitudeCellId = cellIdMod360; 259 if (cellIdMod360 > (maxLongitudeCellId % 360)) maxLongitudeCellId = cellIdMod360; 260 261 if (cellId < minLatitudeCellId) minLatitudeCellId = cellId; 262 if (cellId > maxLatitudeCellId) maxLatitudeCellId = cellId; 263 } 264 double minLongitude = toBoundingBox(minLongitudeCellId).minLong; 265 double minLatitude = toBoundingBox(minLatitudeCellId).minLat; 266 double maxLongitude = toBoundingBox(maxLongitudeCellId).maxLong; 267 double maxLatitude = toBoundingBox(maxLatitudeCellId).maxLat; 268 269 return new LatLngBoundingBox(minLongitude, minLatitude, maxLongitude, maxLatitude); 270 } 271 272 /** 273 * Returns the cell id and centi cell id for the supplied bounding box, Returning null if the supplied bounding box 274 * doesnt enclose a single cell. If the bounding box encloses a single cell but not a centi cell, a Integer[] of 275 * length 1 is returned with containing the cell id. Otherwise a Integer array of length 2, with Integer[0] being the 276 * cell id, Integer[1] being the centi cell. 277 */ 278 public static Integer[] getCentiCellIdForBoundingBox(double minLongitude, double minLatitude, double maxLongitude, 279 double maxLatitude) throws UnableToGenerateCellIdException { 280 281 //int[] maxMinCellIds = getMinMaxCellIdsForBoundingBox(minLongitude, minLatitude, maxLongitude, maxLatitude); 282 //if(maxMinCellIds==null || (maxMinCellIds[0]!=maxMinCellIds[1])) 283 // return null; 284 285 //Integer cellId = maxMinCellIds[0]; 286 287 //int[] maxMinCellIds = getMinMaxCellIdsForBoundingBox(minLongitude, minLatitude, maxLongitude, maxLatitude); 288 289 if (!isBoundingBoxCentiCell(minLongitude, minLatitude, maxLongitude, maxLatitude)) { 290 return null; 291 } 292 293 //ascertain whether bounding box is 0.1 by 0.1 294 //if(isBoundingBoxCentiCell(minLongitude, minLatitude, maxLongitude, maxLatitude)){ 295 296 int[] maxMinCellIds = getMinMaxCellIdsForBoundingBox(minLongitude, minLatitude, maxLongitude, maxLatitude); 297 Integer cellId = maxMinCellIds[0]; 298 299 int minCentiCell = toCentiCellId(minLatitude, minLongitude); 300 int maxCentiCell = toCentiCellId(maxLatitude, maxLongitude); 301 302 double maxLongitude10 = maxLongitude * 10; 303 double maxLatitude10 = maxLatitude * 10; 304 305 if (Math.ceil(maxLatitude10) == Math.floor(maxLatitude10) && Math.ceil(maxLongitude10) == Math.floor(maxLongitude10) 306 && maxCentiCell > 0) { 307 308 //the maxLongitude,maxLatitude point is on a centi cell intersection, hence the maxCentiCellId should be 309 // maxCentiCellId-11 i.e. the cell that is 310 // 1 below and 1 to the left of the centi cell id retrieved 311 //unless it is the 100 centi cell. 312 if (maxCentiCell > minCentiCell) { 313 maxCentiCell = maxCentiCell - 11; 314 } else { 315 maxCentiCell = maxCentiCell + 9; 316 } 317 } 318 319 //if(maxCentiCell==minCentiCell){ 320 return new Integer[] {cellId, minCentiCell}; 321 //} 322 //} 323 //return new Integer[]{cellId}; 324 } 325 326 private static boolean isBoundingBoxCentiCell(double minLongitude, double minLatitude, double maxLongitude, 327 double maxLatitude) { 328 double width = maxLongitude > minLongitude ? maxLongitude - minLongitude : minLongitude - maxLongitude; 329 double height = maxLatitude > minLatitude ? maxLatitude - minLatitude : minLatitude - maxLatitude; 330 return Precision.round(height, 1) == 0.1f && Precision.round(width, 1) == 0.1f; 331 } 332 333 /** 334 * For ease of conversion 335 * 336 * @param args See usage 337 */ 338 public static void main(String[] args) { 339 try { 340 if (args.length == 1) { 341 LatLngBoundingBox llbb = toBoundingBox(Integer.parseInt(args[0])); 342 System.out.println( 343 "CellId " + args[0] + ": minX[" + llbb.getMinLong() + "] minY[" + llbb.getMinLat() + "] maxX[" + llbb 344 .getMaxLong() + "] maxY[" + llbb.getMaxLat() + "]"); 345 } else if (args.length == 2) { 346 double lat = Double.parseDouble(args[0]); 347 double lon = Double.parseDouble(args[1]); 348 System.out.println("lat[" + lat + "] long[" + lon + "] = cellId: " + toCellId(lat, lon)); 349 } else { 350 System.out.println("Provide either a 'cell id' or 'Lat Long' params!"); 351 } 352 } catch (NumberFormatException | UnableToGenerateCellIdException e) { 353 LOGGER.error("Error converting bounding box", e); 354 } 355 } 356}