View Javadoc
1   package net.sumaris.core.dao.referential.location;
2   
3   /*
4    * #%L
5    * SIH-Adagio :: Shared
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2012 - 2014 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU General Public License as
13   * published by the Free Software Foundation, either version 3 of the
14   * License, or (at your option) any later version.
15   * 
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * 
21   * You should have received a copy of the GNU General Public
22   * License along with this program.  If not, see
23   * <http://www.gnu.org/licenses/gpl-3.0.html>.
24   * #L%
25   */
26  
27  
28  import com.google.common.base.Preconditions;
29  import com.google.common.base.Strings;
30  import com.google.common.collect.ImmutableSet;
31  import com.google.common.collect.Lists;
32  import com.google.common.collect.Sets;
33  import com.vividsolutions.jts.geom.Coordinate;
34  import com.vividsolutions.jts.geom.Geometry;
35  import net.sumaris.core.exception.SumarisTechnicalException;
36  import net.sumaris.core.util.Geometries;
37  import org.apache.commons.lang3.StringUtils;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  import org.springframework.core.io.Resource;
41  import org.springframework.core.io.ResourceLoader;
42  
43  import java.io.BufferedReader;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.io.InputStreamReader;
47  import java.util.ArrayList;
48  import java.util.List;
49  import java.util.Set;
50  import java.util.stream.Collectors;
51  
52  /**
53   * <p>Location helper.</p>
54   *
55   * @author Tony Chemit (chemit@codelutin.com)
56   * @author Benoit Lavenier (benoit.lavenier@e-is.pro)
57   */
58  public class Locations {
59      /**
60       * Logger.
61       */
62      private static final Logger log = LoggerFactory.getLogger(Locations.class);
63  
64      /**
65       * <p>Constructor for LocationUtils.</p>
66       */
67      protected Locations() {
68          // Should not be instantiate
69      }
70  
71      /**
72       * Return location label from a longitude and a latitude (in decimal degrees - WG84).
73       *
74       * @param latitude  a latitude (in decimal degrees - WG84)
75       * @param longitude a longitude (in decimal degrees - WG84)
76       * @return A label (corresponding to a statistical rectangle), or null if no statistical rectangle exists for this position
77       */
78      public static String getRectangleLabelByLatLong(Number latitude, Number longitude) {
79          if (longitude == null || latitude == null) {
80              return null;
81          }
82          String locationLabel = null;
83          double lat = latitude.doubleValue();
84          double lon = longitude.doubleValue();
85  
86          // If position inside "Mediterranean and black sea" :
87          if (((lon >= 0 && lon < 42) && (lat >= 30 && lat < 47.5))
88                  || ((lon >= -6 && lon < 0) && (lat >= 35 && lat < 40))) {
89  
90              // Number of rectangles, between the given latitude and 30°N :
91              double nbdemidegreeLat = Math.floor((lat - 30)) * 2;
92  
93              // Number of rectangles, between the given longitude and 6°W :
94              double nbdemidegreeLong = Math.floor((lon + 6)) * 2;
95  
96              // Letter change every 10 rectangles, starting with 'A' :
97              char letter = (char) ((int) (Math.floor(nbdemidegreeLong / 10) + 65));
98              int rest = (int) (nbdemidegreeLong % 10);
99              locationLabel = "M" + ((int) nbdemidegreeLat) + letter + rest; //$NON-NLS-1$
100         }
101 
102         // If position inside "Atlantic (nord-east)" :
103         else if ((lon >= -50 && lon <= 70) && (lat >= 36 && lat <= 89)) {
104             int halfDegreesNb = (int) Math.floor((lat - 36) * 2) + 1;
105             double degreesNb = Math.floor(lon + 50);
106             char letter = (char) ((int) (Math.floor(degreesNb / 10) + 65));
107             int rest = (int) (degreesNb % 10);
108             locationLabel = String.valueOf(halfDegreesNb) + letter + rest;
109         }
110         return locationLabel;
111     }
112 
113 
114     public static Geometry getGeometryFromRectangleLabel(String rectangleLabel) {
115         return getGeometryFromRectangleLabel(rectangleLabel, false);
116     }
117 
118     public static Geometry getGeometryFromRectangleLabel(String rectangleLabel, boolean useMultiPolygon) {
119         Preconditions.checkNotNull(rectangleLabel);
120         Preconditions.checkArgument(StringUtils.isNotBlank(rectangleLabel), "Argument 'rectangleLabel' must not be empty string.");
121 
122         Geometry geometry;
123 
124         // If rectangle inside "Mediterranean and black sea"
125         if (rectangleLabel.startsWith("M")) {
126             String rectangleLabelNoLetter = rectangleLabel.substring(1);
127             String nbdemidegreeLat = rectangleLabelNoLetter.substring(0, 2);
128             String letter = rectangleLabelNoLetter.substring(2, 3);
129             String rest = rectangleLabelNoLetter.substring(3);
130 
131             double latitude = Double.parseDouble(nbdemidegreeLat) * 0.5f + 30;
132 
133             double longitude = Double.parseDouble(rest) * 0.5f + (letter.charAt(0) - 65) * 5 - 6f;
134             geometry = Geometries.createRectangleGeometry(longitude, latitude, longitude + 0.5f, latitude + 0.5f, useMultiPolygon /*=MultiPolygon*/);
135         }
136 
137         // If rectangle inside "Atlantic (nord-east)" :
138         else {
139             String nbdemidegreeLat = rectangleLabel.substring(0, 2);
140             String letter = rectangleLabel.substring(2, 3);
141             String rest = rectangleLabel.substring(3);
142 
143             // Special case for '102D0'
144             if (rectangleLabel.length() == 5) {
145                 nbdemidegreeLat = rectangleLabel.substring(0, 3);
146                 letter = rectangleLabel.substring(3, 4);
147                 rest = rectangleLabel.substring(4);
148             }
149 
150             double latitude = Double.parseDouble(nbdemidegreeLat) * 0.5f + 35.5f;
151             double longitude = Double.parseDouble(rest) + (letter.charAt(0) - 65) * 10 - 50;
152 
153             geometry = Geometries.createRectangleGeometry(longitude, latitude, longitude + 1, latitude + 0.5f, useMultiPolygon /*=MultiPolygon*/);
154         }
155 
156         return geometry;
157     }
158 
159     /**
160      * Compute the polygon from the 10 min x 10 min square label.
161      *
162      * @param square1010 10x10 minutes square label
163      * @deprecated use getGeometryFromMinuteSquareLabel() instead
164      */
165     @Deprecated
166     public static Geometry getGeometryFromSquare10Label(String square1010) {
167         return getGeometryFromMinuteSquareLabel(square1010, 10, true);
168     }
169 
170     public static Geometry getGeometryFromMinuteSquareLabel(String label, int minute, boolean useMultiPolygon) {
171         Preconditions.checkNotNull(label);
172         Preconditions.checkArgument(StringUtils.isNotBlank(label));
173         if (minute < 10) {
174             Preconditions.checkArgument(label.length() == 10, String.format("Invalid square format. Expected 10 characters for %s'x%s' square", minute, minute));
175         }
176         else {
177             Preconditions.checkArgument(label.length() == 8, String.format("Invalid square format. Expected 8 characters for %s'x%s' square", minute, minute));
178         }
179 
180         int cadran = Integer.parseInt(label.substring(0, 1));
181 
182         double signLongitude;
183         double signLatitude;
184         switch (cadran) {
185             case 1: // NW
186                 signLatitude = 1.0;
187                 signLongitude = -1.0;
188                 break;
189             case 2: // NE
190                 signLatitude = 1.0;
191                 signLongitude = 1.0;
192                 break;
193             case 3: // SE
194                 signLatitude = -1.0;
195                 signLongitude = 1.0;
196                 break;
197             case 4: // SW
198                 signLatitude = -1.0;
199                 signLongitude = -1.0;
200                 break;
201             default:
202                 throw new IllegalArgumentException("Unable to parse quadrant");
203         }
204 
205         int offset = 1;
206         int nbMinuteChar = minute < 10 ? 2 : 1;
207 
208         /* Compute the latitude of the square */
209         double intLatitude = Double.parseDouble(label.substring(offset, offset+2));
210         offset += 2;
211         double decLatitude = Double.parseDouble(label.substring(offset, offset + nbMinuteChar)) * minute / 60.0;
212         offset += nbMinuteChar;
213         double latitude = signLatitude * (intLatitude + decLatitude);
214 
215         /* Compute the longitude of the square */
216         double intLongitude = Double.parseDouble(label.substring(offset, offset+3));
217         offset += 3;
218         double decLongitude = Double.parseDouble(label.substring(offset, offset+nbMinuteChar)) * minute / 60.0;
219         double longitude = signLongitude * (intLongitude + decLongitude);
220 
221         return Geometries.createRectangleGeometry(
222                 longitude,
223                 latitude,
224                 longitude + signLongitude * minute / 60.0,
225                 latitude + signLatitude * minute / 60.0,
226                 useMultiPolygon /*=MultiPolygon*/);
227     }
228 
229     /**
230      * Compute the statistical rectangle from the 10x10 square.
231      * (See doc: square_10.md)
232      * @param squareLabel 10x10 square
233      */
234     public static String convertMinuteSquareToRectangle(final String squareLabel, final int minute) {
235         String calculRectangle = "";
236 
237         if (squareLabel == null || squareLabel.length() != 8) {
238             return calculRectangle;
239         }
240 
241         int cadran = Integer.parseInt(squareLabel.substring(0, 1));
242 
243         double signLongitude;
244         double signLatitude;
245         switch (cadran) {
246             case 1: // NW
247                 signLatitude = 1.0;
248                 signLongitude = -1.0;
249                 break;
250             case 2: // NE
251                 signLatitude = 1.0;
252                 signLongitude = 1.0;
253                 break;
254             case 3: // SE
255                 signLatitude = -1.0;
256                 signLongitude = 1.0;
257                 break;
258             case 4: // SW
259                 signLatitude = -1.0;
260                 signLongitude = -1.0;
261                 break;
262             default:
263                 throw new IllegalArgumentException("Unable to parse quadrant");
264         }
265 
266         int offset = 1;
267         int nbMinuteChar = minute < 10 ? 2 : 1;
268         /* Compute the latitude of the square */
269         double intLatitude = Double.parseDouble(squareLabel.substring(offset, offset+2));
270         offset += 2;
271         double decLatitude = Double.parseDouble(squareLabel.substring(offset, offset + nbMinuteChar)) * minute / 60.0;
272         offset += nbMinuteChar;
273         double latitude = signLatitude * (intLatitude + decLatitude);
274 
275         /* Compute the longitude of the square */
276         double intLongitude = Double.parseDouble(squareLabel.substring(offset, offset+3));
277         offset += 3;
278         double decLongitude = Double.parseDouble(squareLabel.substring(offset, offset + nbMinuteChar)) * minute / 60.0;
279         double longitude = signLongitude * (intLongitude + decLongitude);
280 
281         return getRectangleLabelByLatLong(latitude, longitude);
282     }
283 
284     /**
285      * Compute the statistical rectangle from the 10x10 square.
286      * (See doc: square_10.md)
287      * @param square1010 10x10 square
288      */
289     public static String convertSquare10ToRectangle(final String square1010) {
290         return convertMinuteSquareToRectangle(square1010, 10);
291     }
292 
293     /**
294      * Compute the list of suare 10'10' from a statistical rectangle
295      *
296      * @param rectangleLabel rectangle label
297      */
298     public static Set<String> convertRectangleToSquares10(final String rectangleLabel) {
299         return convertRectangleToMinuteSquares(rectangleLabel, 10);
300     }
301 
302 
303     public static Set<String> getAllIcesRectangleLabels(ResourceLoader resourceLoader, boolean failSafe) {
304 
305         try {
306             return readLines(resourceLoader.getResource("classpath:referential/ices_rectangles.txt"));
307         } catch (SumarisTechnicalException e) {
308             if (failSafe) {
309                 // continue
310             } else {
311                 throw e;
312             }
313         }
314         // Fail safe: will generate all rectangle (not only in sea area !)
315         Set<String> result = Sets.newHashSet();
316         StringBuilder sb = new StringBuilder();
317         for (int i = 1; i <= 98; i++) {
318             sb.setLength(0);
319             sb.append(i < 10 ? ("0" + i) : i);
320             for (char l = 'A'; l <= 'M'; l++) {
321                 sb.setLength(2);
322                 sb.append(l);
323                 for (int j = 0; j <= 8; j++) {
324                     sb.setLength(3);
325                     sb.append(j);
326                     result.add(sb.toString());
327                 }
328             }
329         }
330 
331         return result;
332     }
333 
334     public static Set<String> getAllCgpmGfcmRectangleLabels(ResourceLoader resourceLoader, boolean failSafe) {
335         try {
336             return readLines(resourceLoader.getResource("classpath:referential/cgpm_rectangles.txt"));
337         } catch (SumarisTechnicalException e) {
338             if (failSafe) {
339                 // continue
340             } else {
341                 throw e;
342             }
343         }
344 
345         // Fail safe: will generate all rectangle (not only in sea area !)
346         Set<String> result = Sets.newHashSet();
347         StringBuilder sb = new StringBuilder();
348         sb.append("M");
349         for (int i = 0; i <= 34; i++) {
350             sb.setLength(1);
351             sb.append(i < 10 ? ("0" + i) : i);
352             for (char l = 'A'; l <= 'J'; l++) {
353                 sb.setLength(3);
354                 sb.append(l);
355                 for (int j = 0; j <= 8; j++) {
356                     sb.setLength(4);
357                     sb.append(j);
358                     result.add(sb.toString());
359                 }
360             }
361         }
362 
363         return result;
364     }
365 
366     /**
367      * Return location label (square 10'x10' format) from a longitude and a latitude (in decimal degrees - WG84).
368      *
369      * @param latitude  a latitude (in decimal degrees - WG84)
370      * @param longitude a longitude (in decimal degrees - WG84)
371      * @return A label
372      */
373     public static String getSquare10LabelByLatLong(Number latitude, Number longitude) {
374         return getMinuteSquareLabelByLatLong(latitude, longitude, 10);
375     }
376 
377     /**
378      * Return square label from a longitude and a latitude (in decimal degrees - WG84).
379      *
380      * @param latitude  a latitude (in decimal degrees - WG84)
381      * @param longitude a longitude (in decimal degrees - WG84)
382      * @param squareSize Size of the square, in minutes
383      * @return A label
384      */
385     public static String getMinuteSquareLabelByLatLong(Number latitude, Number longitude, int squareSize) {
386         if (longitude == null || latitude == null) {
387             return null;
388         }
389         double lat = latitude.doubleValue();
390         double lon = longitude.doubleValue();
391         StringBuilder result = new StringBuilder();
392 
393         // Quadrant
394         int quadrant;
395         if (lon <= 0 && lat >= 0) {
396             quadrant = 1;
397         }
398         else if (lon > 0 && lat > 0) {
399             quadrant = 2;
400         } else if (lon > 0 && lat < 0) {
401             quadrant = 3;
402         } else {
403             quadrant = 4;
404         }
405         result.append(quadrant);
406 
407         // Latitude
408         lat = Math.abs(lat);
409         int intLatitude = (int)Math.floor(lat);
410         int decLatitude = (int)Math.floor((lat - intLatitude) * 60 / squareSize);
411         result.append(Strings.padStart(String.valueOf(intLatitude), 2, '0'));
412         if (squareSize >= 10) {
413             // minute in one character
414             result.append(decLatitude);
415         }
416         else {
417             // minute in two character
418             result.append(Strings.padStart(String.valueOf(decLatitude), 2, '0'));
419         }
420 
421         // Longitude
422         lon = Math.abs(lon);
423         int intLongitude = (int)Math.floor(lon);
424         int decLongitude = (int)Math.floor((lon - intLongitude) * 60 / squareSize);
425         result.append(Strings.padStart(String.valueOf(intLongitude), 3, '0'));
426         if (squareSize >= 10) {
427             // minute in one character
428             result.append(decLongitude);
429         }
430         else {
431             // minute in two character
432             result.append(Strings.padStart(String.valueOf(decLongitude), 2, '0'));
433         }
434 
435         return result.toString();
436     }
437 
438     public static Set<String> getAllSquare10Labels(ResourceLoader resourceLoader, boolean failSafe) {
439         return ImmutableSet.<String>builder()
440                 .addAll(getAllIcesRectangleLabels(resourceLoader, failSafe))
441                 .addAll(getAllCgpmGfcmRectangleLabels(resourceLoader, failSafe))
442                 .build().stream()
443                 .flatMap(label -> Locations.convertRectangleToSquares10(label).stream())
444                 .collect(Collectors.toSet());
445     }
446 
447     /* -- private methods -- */
448 
449     public static Set<String> convertRectangleToMinuteSquares(final String rectangleLabel, final int minute) {
450         Preconditions.checkNotNull(rectangleLabel);
451         Preconditions.checkArgument(StringUtils.isNotBlank(rectangleLabel), "Argument 'rectangleLabel' must not be empty string.");
452 
453         Geometry geom = getGeometryFromRectangleLabel(rectangleLabel, false);
454 
455         if (geom == null) return null;
456 
457         Coordinate startPoint = geom.getCoordinates()[0];
458         Coordinate endPoint = geom.getCoordinates()[2];
459 
460         Set<String> result = Sets.newHashSet();
461 
462         for (double longitude = startPoint.x; longitude < endPoint.x; longitude += minute/60.) {
463             for (double latitude = startPoint.y; latitude < endPoint.y; latitude += minute/60.) {
464                 String label = getSquare10LabelByLatLong(latitude, longitude);
465                 result.add(label);
466             }
467         }
468 
469         return result;
470     }
471 
472     private static Set<String> readLines(Resource resource) {
473         Preconditions.checkNotNull(resource);
474         Preconditions.checkArgument(resource.exists());
475         Set<String> result = Sets.newHashSet();
476         try {
477             InputStream is = resource.getInputStream();
478             BufferedReader reader = new BufferedReader(new InputStreamReader(is));
479             String line = reader.readLine();
480             while (line != null) {
481                 if (StringUtils.isNotBlank(line)) {
482                     result.add(line.trim());
483                 }
484                 line = reader.readLine();
485             }
486             return result;
487         } catch (IOException e) {
488             throw new SumarisTechnicalException("Could not read resource: " + resource.getFilename(), e);
489         }
490     }
491 
492 
493 }