]> gitweb.fperrin.net Git - GpsPrune.git/commitdiff
Version 20.4, May 2021 master
authoractivityworkshop <mail@activityworkshop.net>
Mon, 10 May 2021 17:41:33 +0000 (19:41 +0200)
committeractivityworkshop <mail@activityworkshop.net>
Mon, 10 May 2021 17:41:33 +0000 (19:41 +0200)
46 files changed:
README.md
buildtools/build.sh
buildtools/pom.xml
buildtools/version.properties
src/tim/prune/GpsPrune.java
src/tim/prune/data/RangeStats.java
src/tim/prune/function/srtm/LookupSrtmFunction.java
src/tim/prune/gui/map/MapCanvas.java
src/tim/prune/gui/map/MapSource.java
src/tim/prune/gui/map/MffMapSource.java
src/tim/prune/gui/map/OsmMapSource.java
src/tim/prune/gui/map/SiteNameUtils.java [new file with mode: 0644]
src/tim/prune/lang/prune-texts_af.properties
src/tim/prune/lang/prune-texts_cz.properties
src/tim/prune/lang/prune-texts_de.properties
src/tim/prune/lang/prune-texts_de_CH.properties
src/tim/prune/lang/prune-texts_en.properties
src/tim/prune/lang/prune-texts_es.properties
src/tim/prune/lang/prune-texts_fi.properties
src/tim/prune/lang/prune-texts_fr.properties
src/tim/prune/lang/prune-texts_hu.properties
src/tim/prune/lang/prune-texts_it.properties
src/tim/prune/lang/prune-texts_ja.properties
src/tim/prune/lang/prune-texts_ko.properties
src/tim/prune/lang/prune-texts_nl.properties
src/tim/prune/lang/prune-texts_pl.properties
src/tim/prune/lang/prune-texts_pt.properties
src/tim/prune/lang/prune-texts_ro.properties
src/tim/prune/lang/prune-texts_ru.properties
src/tim/prune/lang/prune-texts_sv.properties
src/tim/prune/lang/prune-texts_zh.properties
src/tim/prune/load/BabelFileFormats.java
src/tim/prune/load/FileLoader.java
src/tim/prune/load/json/JsonBlock.java [new file with mode: 0644]
src/tim/prune/load/json/JsonFileLoader.java [new file with mode: 0644]
src/tim/prune/load/json/JsonPoint.java [new file with mode: 0644]
src/tim/prune/readme.txt
src/tim/prune/threedee/Java3DWindow.java
src/tim/prune/threedee/UprightOrbiter.java [new file with mode: 0644]
test/tim/prune/data/RangeStatsTest.java [new file with mode: 0644]
test/tim/prune/function/cache/TileSetTest.java [new file with mode: 0644]
test/tim/prune/function/olc/OlcDecoderTest.java [new file with mode: 0644]
test/tim/prune/function/weather/SingleForecastTest.java [new file with mode: 0644]
test/tim/prune/gui/map/MapSourceTest.java [new file with mode: 0644]
test/tim/prune/gui/map/SiteNameUtilsTest.java [new file with mode: 0644]
test/tim/prune/jpeg/drew/RationalTest.java [new file with mode: 0644]

index 1a1aa06ddf261d3ce0d44b8610156ce1baca0b5a..169f8ee169ed5b88f0435a06c45fdbd70efd83ad 100644 (file)
--- a/README.md
+++ b/README.md
@@ -3,5 +3,5 @@ GpsPrune is a map-based application for viewing, editing and converting coordina
 
 It's a cross-platform java application, and its home page is at https://gpsprune.activityworkshop.net .
 
-Here on github you'll find all the sources from version 1 to the current version 20, and in the wiki at https://github.com/activityworkshop/GpsPrune/wiki there's the beginning of a translation effort for anyone to contribute.
-Currently just the Spanish translations and some missing French texts are online, to see whether it's a workable idea or not.  Please help with these if you can.
+Here on github you'll find all the sources from version 1 to the current version 20.4, and in the wiki at https://github.com/activityworkshop/GpsPrune/wiki there's the beginning of a translation effort for anyone to contribute.
+Currently just the missing French texts and Polish texts are online, to see whether it's a workable idea or not.  Please help with these if you can.
index 193244bb3319fa81e974dd1d9f09fbbb52c7f70c..f4f00f9e4fd17d9ad409629b13fcf594ddcc8b5c 100644 (file)
@@ -1,7 +1,7 @@
 # Build script
 set -e
 # Version number
-PRUNENAME=gpsprune_20
+PRUNENAME=gpsprune_20.4
 # remove compile directory
 rm -rf compile
 # remove dist directory
index 15b6562639083b2eba02a431e820e46a75c8e0e9..b079793cd51a16cb2eda1327760d61aff09fc16c 100644 (file)
@@ -7,7 +7,7 @@
 
        <groupId>tim.prune</groupId>
        <artifactId>gpsprune</artifactId>
-       <version>20</version>
+       <version>20.4</version>
        <packaging>jar</packaging>
 
        <name>tim.prune.gpsprune</name>
                        <artifactId>j3dutils</artifactId>
                        <version>${j3dutils.version}</version>
                </dependency>
+               <dependency>
+                 <groupId>org.junit.jupiter</groupId>
+                 <artifactId>junit-jupiter-engine</artifactId>
+                 <version>5.7.1</version>
+                 <scope>test</scope>
+               </dependency>
+               <dependency>
+                 <groupId>org.junit.jupiter</groupId>
+                 <artifactId>junit-jupiter-api</artifactId>
+                 <version>5.7.1</version>
+                 <scope>test</scope>
+               </dependency>
        </dependencies>
 
        <build>
                <outputDirectory>${project.build.directory}/classes</outputDirectory>
                <finalName>${project.artifactId}_${project.version}</finalName>
-               <sourceDirectory>${project.basedir}/</sourceDirectory>
+               <sourceDirectory>${project.basedir}/src</sourceDirectory>
+               <testSourceDirectory>${project.basedir}/test</testSourceDirectory>
                <resources>
                        <resource>
                                <directory>${project.basedir}/src/</directory>
                                <plugin>
                                        <artifactId>maven-compiler-plugin</artifactId>
                                        <version>3.8.0</version>
+                                       <configuration>
+                                         <compilerArgs>
+                                           <arg>-Xlint:deprecation</arg>
+                                         </compilerArgs>
+                                       </configuration>
                                </plugin>
                                <plugin>
                                        <artifactId>maven-jar-plugin</artifactId>
                                                <mainClass>${app.mainClass}</mainClass>
                                        </configuration>
                                </plugin>
-
+                               <plugin>
+                                 <artifactId>maven-surefire-plugin</artifactId>
+                                 <version>2.22.2</version>
+                               </plugin>
+                               <plugin>
+                                 <artifactId>maven-failsafe-plugin</artifactId>
+                                 <version>2.22.2</version>
+                               </plugin>
                        </plugins>
                </pluginManagement>
        </build>
index fe5699b021d0d7d613f4832110fdc957de74e9a9..15c4a262e4ffad7804e90903a7345fdabb6d3218 100644 (file)
@@ -1 +1 @@
-version=20
+version=20.4
index 8d80875b225520c864f01db25b15165f0763176e..6644642cc2168172a78c6bb68f16d724d1aea7a1 100644 (file)
@@ -38,9 +38,9 @@ import tim.prune.gui.profile.ProfileChart;
 public class GpsPrune
 {
        /** Version number of application, used in about screen and for version check */
-       public static final String VERSION_NUMBER = "20.3";
+       public static final String VERSION_NUMBER = "20.4";
        /** Build number, just used for about screen */
-       public static final String BUILD_NUMBER = "385";
+       public static final String BUILD_NUMBER = "387";
        /** Static reference to App object */
        private static App APP = null;
 
index d8461c476b122a58561d215296b1fe7370aef74b..b32f12fa987344a04f2a419e8cb68e7cfd210975 100644 (file)
@@ -11,7 +11,7 @@ public class RangeStats
        private boolean _foundTrackPoint = false;
        protected AltitudeRange _totalAltitudeRange = new AltitudeRange();
        protected AltitudeRange _movingAltitudeRange = new AltitudeRange();
-       private Timestamp _earliestTimestamp = null, _latestTimestamp = null;
+       private Timestamp _earliestTimestamp = null, _latestTimestamp = null, _movingTimestamp = null;
        private long _movingMilliseconds = 0L;
        private boolean _timesIncomplete = false;
        private boolean _timesOutOfSequence = false;
@@ -75,6 +75,12 @@ public class RangeStats
                }
 
                // timestamps
+               if (inPoint.getSegmentStart())
+               {
+                       // reset movingTimestamp for moving time at the start
+                       // of each segment
+                       _movingTimestamp = null;
+               }
                if (inPoint.hasTimestamp())
                {
                        Timestamp currTstamp = inPoint.getTimestamp();
@@ -84,10 +90,11 @@ public class RangeStats
                        if (_latestTimestamp == null || currTstamp.isAfter(_latestTimestamp)) {
                                _latestTimestamp = currTstamp;
                        }
+
                        // Work out duration without segment gaps
-                       if (!inPoint.getSegmentStart() && _prevPoint != null && _prevPoint.hasTimestamp())
+                       if (_movingTimestamp != null)
                        {
-                               long millisLater = currTstamp.getMillisecondsSince(_prevPoint.getTimestamp());
+                               long millisLater = currTstamp.getMillisecondsSince(_movingTimestamp);
                                if (millisLater < 0) {
                                        _timesOutOfSequence = true;
                                }
@@ -95,6 +102,7 @@ public class RangeStats
                                        _movingMilliseconds += millisLater;
                                }
                        }
+                       _movingTimestamp = currTstamp;
                }
                else {
                        _timesIncomplete = true;
index c5986dba48710517b5432d8a9776370764857ac5..cccc7ede7dca98a6b9fda70d9d484fdbc7b2b876 100644 (file)
@@ -267,7 +267,7 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable
                // try to download to cache
                TileDownloader cacher = new TileDownloader();
                TileDownloader.Result result = cacher.downloadTile(inUrl);
-               System.out.println("Result: " + result);
+               // System.out.println("Result: " + result);
                if (result == TileDownloader.Result.DOWNLOADED)
                {
                        _numCached++;
index a469e99d014c4254d3c5529402589baa58ab05df..17918924f92e793f8100a8df5771aedbe39bd880 100644 (file)
@@ -691,8 +691,9 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe
                final int[] xPixels = new int[numPoints];
                final int[] yPixels = new int[numPoints];
 
-               final int pointSeparationForArrowsSqd = 350;
+               final int pointSeparationForArrowsSqd = 400;
                final int pointSeparation1dForArrows = (int) (Math.sqrt(pointSeparationForArrowsSqd) * 0.7);
+               final int hugePointSeparationForArrows = 120;
 
                // try to set line width for painting
                if (inG instanceof Graphics2D)
@@ -765,35 +766,38 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe
                                        pointsPainted = true;
 
                                        // Now consider whether we need to draw an arrow as well
-                                       if (drawArrows
-                                        && !drawnLastArrow
-                                        && (Math.abs(prevX-px) > pointSeparation1dForArrows || Math.abs(prevY-py) > pointSeparation1dForArrows))
+                                       if (drawArrows)
                                        {
-                                               final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py);
-                                               if (pointSeparationSqd > pointSeparationForArrowsSqd)
+                                               final double pointDist = Math.max(Math.abs(prevX - px), Math.abs(prevY - py));
+                                               final int separationLimit = (drawnLastArrow ? hugePointSeparationForArrows : pointSeparation1dForArrows);
+                                               if (pointDist > separationLimit)
                                                {
-                                                       final double midX = (prevX + px) / 2.0;
-                                                       final double midY = (prevY + py) / 2.0;
-                                                       final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight;
-                                                       if (midPointVisible)
+                                                       final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py);
+                                                       if (pointSeparationSqd > pointSeparationForArrowsSqd)
                                                        {
-                                                               final double alpha = Math.atan2(py - prevY, px - prevX);
-                                                               //System.out.println("Draw arrow from (" + prevX + "," + prevY + ") to (" + px + "," + py
-                                                               //      + ") with angle" + (int) (alpha * 180/Math.PI));
-                                                               final double MID_TO_VERTEX = 3.0;
-                                                               final double arrowX = MID_TO_VERTEX * Math.cos(alpha);
-                                                               final double arrowY = MID_TO_VERTEX * Math.sin(alpha);
-                                                               final double vertexX = midX + arrowX;
-                                                               final double vertexY = midY + arrowY;
-                                                               inG.drawLine((int)(midX-arrowX-2*arrowY), (int)(midY-arrowY+2*arrowX), (int)vertexX, (int)vertexY);
-                                                               inG.drawLine((int)(midX-arrowX+2*arrowY), (int)(midY-arrowY-2*arrowX), (int)vertexX, (int)vertexY);
+                                                               final double midX = (prevX + px) / 2.0;
+                                                               final double midY = (prevY + py) / 2.0;
+                                                               final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight;
+                                                               if (midPointVisible)
+                                                               {
+                                                                       final double alpha = Math.atan2(py - prevY, px - prevX);
+                                                                       //System.out.println("Draw arrow from (" + prevX + "," + prevY + ") to (" + px + "," + py
+                                                                       //      + ") with angle" + (int) (alpha * 180/Math.PI));
+                                                                       final double MID_TO_VERTEX = 3.0;
+                                                                       final double arrowX = MID_TO_VERTEX * Math.cos(alpha);
+                                                                       final double arrowY = MID_TO_VERTEX * Math.sin(alpha);
+                                                                       final double vertexX = midX + arrowX;
+                                                                       final double vertexY = midY + arrowY;
+                                                                       inG.drawLine((int)(midX-arrowX-2*arrowY), (int)(midY-arrowY+2*arrowX), (int)vertexX, (int)vertexY);
+                                                                       inG.drawLine((int)(midX-arrowX+2*arrowY), (int)(midY-arrowY-2*arrowX), (int)vertexX, (int)vertexY);
+                                                               }
+                                                               drawnLastArrow = midPointVisible;
                                                        }
-                                                       drawnLastArrow = midPointVisible;
                                                }
-                                       }
-                                       else
-                                       {
-                                               drawnLastArrow = false;
+                                               else
+                                               {
+                                                       drawnLastArrow = false;
+                                               }
                                        }
                                }
                                prevX = px; prevY = py;
index c4e294665e128def7d1fcbfb048cb516624d5d00..aa7127dee7020a70c9adb42f3a077e99075b6b8b 100644 (file)
@@ -2,8 +2,7 @@ package tim.prune.gui.map;
 
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+
 
 /**
  * Class to represent any map source, whether an OsmMapSource
@@ -16,9 +15,6 @@ public abstract class MapSource
        /** File extensions */
        protected String[] _extensions = null;
 
-       /** Regular expression for catching server wildcards */
-       protected static final Pattern WILD_PATTERN = Pattern.compile("^(.*)\\[(.*)\\](.*)$");
-
 
        /**
         * @return the number of layers used in this source
@@ -120,32 +116,6 @@ public abstract class MapSource
                return urlstr;
        }
 
-       /**
-        * Fix the site name by stripping off protocol and www.
-        * This is used to create the file path for disk caching
-        * @param inUrl url to strip
-        * @return stripped url
-        */
-       protected static String fixSiteName(String inUrl)
-       {
-               if (inUrl == null || inUrl.equals("")) {return null;}
-               String url = inUrl.toLowerCase();
-               int idx = url.indexOf("://");
-               if (idx >= 0) {url = url.substring(idx + 3);}
-               if (url.startsWith("www.")) {url = url.substring(4);}
-               // Strip out any "[.*]" as well
-               if (url.indexOf('[') >= 0)
-               {
-                       Matcher matcher = WILD_PATTERN.matcher(url);
-                       if (matcher.matches()) {
-                               url = matcher.group(1) + matcher.group(3);
-                               if (url.length() > 1 && url.charAt(0) == '.') {
-                                       url = url.substring(1);
-                               }
-                       }
-               }
-               return url;
-       }
 
        /**
         * @return string which can be written to the Config
index 42806fc75e7e104c8fa2c3140fc9ad9d0deec459..fe3e5a6b45ef79d1a971b7d728536245b889c067 100644 (file)
@@ -32,8 +32,8 @@ public class MffMapSource extends MapSource
                _baseUrls[0] = fixBaseUrl(inUrl1);
                _baseUrls[1] = fixBaseUrl(inUrl2);
                _siteNames = new String[2];
-               _siteNames[0] = fixSiteName(_baseUrls[0]);
-               _siteNames[1] = fixSiteName(_baseUrls[1]);
+               _siteNames[0] = SiteNameUtils.convertUrlToDirectory(_baseUrls[0]);
+               _siteNames[1] = SiteNameUtils.convertUrlToDirectory(_baseUrls[1]);
                _extensions = new String[2];
                _extensions[0] = inExt1;
                _extensions[1] = inExt2;
index 925fcf623ea456154c512c5e5e89454eb5b46bf8..0cb579dfe271731073665397f8072ec92c1cba65 100644 (file)
@@ -1,6 +1,5 @@
 package tim.prune.gui.map;
 
-import java.util.regex.Matcher;
 import tim.prune.I18nManager;
 
 /**
@@ -84,8 +83,8 @@ public class OsmMapSource extends MapSource
                _extensions[0] = inExt1;
                _extensions[1] = inExt2;
                _siteNames = new String[2];
-               _siteNames[0] = fixSiteName(_baseUrls[0]);
-               _siteNames[1] = fixSiteName(_baseUrls[1]);
+               _siteNames[0] = SiteNameUtils.convertUrlToDirectory(_baseUrls[0]);
+               _siteNames[1] = SiteNameUtils.convertUrlToDirectory(_baseUrls[1]);
                // Swap layers if second layer given without first
                if (_baseUrls[0] == null && _baseUrls[1] != null)
                {
@@ -157,7 +156,7 @@ public class OsmMapSource extends MapSource
        {
                // Check if the base url has a [1234], if so replace at random
                StringBuffer url = new StringBuffer();
-               url.append(pickServerUrl(_baseUrls[inLayerNum]));
+               url.append(SiteNameUtils.pickServerUrl(_baseUrls[inLayerNum]));
                url.append(inZoom).append('/').append(inX).append('/').append(inY);
                url.append('.').append(getFileExtension(inLayerNum));
                if (_apiKey != null)
@@ -175,36 +174,6 @@ public class OsmMapSource extends MapSource
                return _maxZoom;
        }
 
-       /**
-        * If the base url contains something like [1234], then pick a server
-        * @param inBaseUrl base url
-        * @return modified base url
-        */
-       protected static final String pickServerUrl(String inBaseUrl)
-       {
-               if (inBaseUrl == null || inBaseUrl.indexOf('[') < 0) {
-                       return inBaseUrl;
-               }
-               // Check for [.*] (once only)
-               // Only need to support one, make things a bit easier
-               final Matcher matcher = WILD_PATTERN.matcher(inBaseUrl);
-               // if not, return base url unchanged
-               if (!matcher.matches()) {
-                       return inBaseUrl;
-               }
-               // if so, pick one at random and replace in the String
-               final String match = matcher.group(2);
-               final int numMatches = match.length();
-               String server = null;
-               if (numMatches > 0)
-               {
-                       int matchNum = (int) Math.floor(Math.random() * numMatches);
-                       server = "" + match.charAt(matchNum);
-               }
-               final String result = matcher.group(1) + (server==null?"":server) + matcher.group(3);
-               return result;
-       }
-
        /**
         * @return semicolon-separated list of all fields
         */
diff --git a/src/tim/prune/gui/map/SiteNameUtils.java b/src/tim/prune/gui/map/SiteNameUtils.java
new file mode 100644 (file)
index 0000000..cf53f72
--- /dev/null
@@ -0,0 +1,73 @@
+package tim.prune.gui.map;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper functions for manipulating tile site names
+ */
+public abstract class SiteNameUtils
+{
+       /** Regular expression for catching server wildcards */
+       private static final Pattern WILD_PATTERN = Pattern.compile("^(.*)\\[(.*)\\](.*)$");
+
+
+       /**
+        * If the base url contains something like [1234], then pick a server
+        * @param inBaseUrl base url
+        * @return modified base url
+        */
+       public static String pickServerUrl(String inBaseUrl)
+       {
+               if (inBaseUrl == null || inBaseUrl.indexOf('[') < 0) {
+                       return inBaseUrl;
+               }
+               // Check for [.*] (once only)
+               // Only need to support one, make things a bit easier
+               final Matcher matcher = WILD_PATTERN.matcher(inBaseUrl);
+               // if not, return base url unchanged
+               if (!matcher.matches()) {
+                       return inBaseUrl;
+               }
+               // if so, pick one at random and replace in the String
+               final String match = matcher.group(2);
+               final int numMatches = match.length();
+               String server = null;
+               if (numMatches > 0)
+               {
+                       int matchNum = (int) Math.floor(Math.random() * numMatches);
+                       server = "" + match.charAt(matchNum);
+               }
+               final String result = matcher.group(1) + (server==null?"":server) + matcher.group(3);
+               return result;
+       }
+
+
+       /**
+        * Fix the site name by stripping off protocol and www.
+        * This is used to create the file path for disk caching
+        * @param inUrl url to strip
+        * @return stripped url
+        */
+       public static String convertUrlToDirectory(String inUrl)
+       {
+               if (inUrl == null || inUrl.equals("")) {return null;}
+               String url = inUrl.toLowerCase();
+               int idx = url.indexOf("://");
+               if (idx >= 0) {url = url.substring(idx + 3);}
+               if (url.startsWith("www.")) {url = url.substring(4);}
+               // Strip out any "[.*]" as well
+               if (url.indexOf('[') >= 0)
+               {
+                       Matcher matcher = WILD_PATTERN.matcher(url);
+                       if (matcher.matches())
+                       {
+                               url = matcher.group(1) + matcher.group(3);
+                               if (url.length() > 1 && url.charAt(0) == '.') {
+                                       url = url.substring(1);
+                               }
+                       }
+               }
+               return url;
+       }
+}
index 03acf63bb8c824f19c88b3f2d5e7deeceae6d480..8fab157828119bb6f028bd31b8fe17eac04d321f 100644 (file)
@@ -814,8 +814,6 @@ error.language.wrongfile=Die geselekteerde leer lyk nie soos 'n taal leer vir Gp
 error.convertnamestotimes.nonames=Geen name kon in tye omgeskakel word
 error.lookupsrtm.nonefound=Geen hoogte waardes beskikbaar vir die punte
 error.lookupsrtm.nonerequired=Al die punte het klaar hoogtes, so daar is niks meer om te soek
-error.gpsies.uploadnotok=Die gpsies server het die boodskap terug gestuur
-error.gpsies.uploadfailed=Die oplaai het misluk met die volgende fout boodskap
 error.showphoto.failed=Foto het nie gelaai nie
 error.playaudiofailed=Klankgreep het nie gespeel nie
 error.cache.notthere=Die te\u00ebl stoorarea gids kon nie opgespoor word nie
index 92afbf4f9a5e83494e6cb35d743984b4f8a86054..39f177e2486a8d6eac80394399c46e2bc8ff1c1b 100644 (file)
@@ -95,6 +95,7 @@ function.convertnamestotimes=P\u0159ev\u00e9st n\u00e1zvy v\u00fdzna\u010dn\u00f
 function.deletefieldvalues=Smazat hodnoty pole
 function.findwaypoint=Hledat bod
 function.pastecoordinates=Zadat sou\u0159adnice
+function.enterpluscode=Zadat plus k\u00f3d
 function.charts=Grafy
 function.show3d=Trojrozm\u011brn\u011b
 function.distances=Vzd\u00e1lenosti
@@ -814,8 +815,6 @@ error.language.wrongfile=Vybran\u00fd soubor nevypad\u00e1 jako jazykov\u00fd so
 error.convertnamestotimes.nonames=N\u00e1zvy nemohou b\u00fdt p\u0159evedeny na \u010dasov\u00e9 zna\u010dky
 error.lookupsrtm.nonefound=Pro tyto body nen\u00ed k dispozici informace o nadmo\u0159sk\u00e9 v\u00fd\u0161ce
 error.lookupsrtm.nonerequired=U v\u0161ech bod\u016f u\u017e je informaci o v\u00fd\u0161ce, tak\u017ee nen\u00ed co dohled\u00e1vat
-error.gpsies.uploadnotok=Server gpsies vr\u00e1til hl\u00e1\u0161en\u00ed
-error.gpsies.uploadfailed=Chyba - nepoda\u0159ilo se nahr\u00e1t data.
 error.showphoto.failed=Nepoda\u0159ilo se na\u010d\u00edst fotografii
 error.playaudiofailed=Nepoda\u0159ilo se p\u0159ehr\u00e1t zvukov\u00fd soubor.
 error.cache.notthere=Nepoda\u0159ilo se nal\u00e9zt adres\u00e1\u0159 s cache map.
index ffc031dccb29d1eec26f56d75de872fd9a845972..10642117cf4752013fc6fdd7b864a3a9f472eec4 100644 (file)
@@ -876,8 +876,6 @@ error.language.wrongfile=Die ausgew\u00e4hlte Datei scheint keine Sprachdatei f\
 error.convertnamestotimes.nonames=Es konnten keine Namen umgewandelt werden
 error.lookupsrtm.nonefound=Keine H\u00f6hendaten verf\u00fcgbar f\u00fcr diese Punkte
 error.lookupsrtm.nonerequired=Alle Punkte haben schon H\u00f6hendaten
-error.gpsies.uploadnotok=Der Gpsies Server hat geantwortet
-error.gpsies.uploadfailed=Das Hochladen ist fehlgeschlagen
 error.showphoto.failed=Das Foto konnte nicht geladen werden
 error.playaudiofailed=Das Abspielen der Audiodatei ist fehlgeschlagen
 error.cache.notthere=Der Ordner wurde nicht gefunden
index 555390e08ea5d6d4d161b88ff7d977146fb6373b..76771e016b18c2c48766c02b8a250cbfbf85b08f 100644 (file)
@@ -871,8 +871,6 @@ error.language.wrongfile=Die uusgew\u00e4hlti Datei scheint kei Sproch-Datei f\u
 error.convertnamestotimes.nonames=Kei Namen han k\u00f6nnet verwondlet werde
 error.lookupsrtm.nonefound=Kei H\u00f6hendate verf\u00fcegbar f\u00fcr d'P\u00fcnkte
 error.lookupsrtm.nonerequired=Alle P\u00fcnkte han die H\u00f6hendate scho.  N\u00fc\u00fct z'tue.
-error.gpsies.uploadnotok=Der Gpsies Server h\u00e4t gseit gha
-error.gpsies.uploadfailed=S Uufalade isch fehlgschlage
 error.showphoto.failed=S F\u00f6teli han i n\u00f6d k\u00f6nne lade
 error.playaudiofailed=S Abschpiele vonem File isch fehlgschlage
 error.cache.notthere=D Ordner isch n\u00f6d gfunde worde
index ad0d8db8e14083b34d5f49754b007e3b5e530460..f2910cfbc6b3f18c77c928d763a8dc671156b570 100644 (file)
@@ -886,8 +886,6 @@ error.language.wrongfile=The selected file doesn't appear to be a language file
 error.convertnamestotimes.nonames=No names could be converted into times
 error.lookupsrtm.nonefound=No altitude values available for these points
 error.lookupsrtm.nonerequired=All points already have altitudes, so there's nothing to lookup
-error.gpsies.uploadnotok=The gpsies server returned the message
-error.gpsies.uploadfailed=The upload failed with the error
 error.showphoto.failed=Failed to load photo
 error.playaudiofailed=Failed to play audio clip
 error.cache.notthere=The tile cache directory was not found
index 6d6b76ed4e65242a778060e5f071a491a8234f6b..9885d80fa0d3f7920a22f52947cb3488e8c46114 100644 (file)
@@ -887,8 +887,6 @@ error.language.wrongfile=El archivo seleccionado no parece ser un archivo de len
 error.convertnamestotimes.nonames=Los nombres no pudieron ser convertidos en tiempos
 error.lookupsrtm.nonefound=No se encontraron valores de altitud
 error.lookupsrtm.nonerequired=Todos los puntos tienen altitudes, as\u00ed que no hay nada que buscar.
-error.gpsies.uploadnotok=El servidor de gpsies ha devuelto el mensaje
-error.gpsies.uploadfailed=La carga ha fallado con el error
 error.showphoto.failed=Fallo al cargar la foto
 error.playaudiofailed=Fallo reproduciendo archivo de audio
 error.cache.notthere=No se encontr\u00f3 la carpeta del cache de recuadros
index 226a84b7a9ad9ae819177a2ee2d073d627f5d711..2b7566ed0c9be8afe0252ee770db8c003042a673 100644 (file)
@@ -100,6 +100,7 @@ function.pastecoordinates=Anna uudet koordinaatit...
 function.charts=Kaaviot
 function.show3d=3D-n\u00e4kym\u00e4
 function.distances=V\u00e4limatkat
+function.viewfulldetails=Kaikki yksityiskohdat
 function.estimatetime=Arvioi aika
 function.learnestimationparams=Opi aika-arvion parametrit
 function.autoplay=Animoi reitti
@@ -514,6 +515,7 @@ dialog.diskcache.table.zoom=Zoom
 dialog.diskcache.table.tiles=Tiles
 dialog.diskcache.table.megabytes=Megabytes
 dialog.diskcache.tileset=Tileset
+dialog.diskcache.tileset.multiple=usea
 dialog.diskcache.deleteold=Poista vanhan karttapalat
 dialog.diskcache.maximumage=Enimm\u00e4isik\u00e4 (p\u00e4ivi\u00e4)
 dialog.diskcache.deleteall=Poista kaikki kartapalat
@@ -702,6 +704,7 @@ map.overzoom=Ei karttoja t\u00e4lle suurennussuhteelle
 # Field names
 fieldname.latitude=Leveysaste (Lat.)
 fieldname.longitude=Pituusaste (Long.)
+fieldname.coordinates=Koordinaatit
 fieldname.altitude=Korkeus
 fieldname.timestamp=Aikaleima
 fieldname.time=Aika
@@ -716,6 +719,7 @@ fieldname.duration=Kesto
 fieldname.speed=Nopeus
 fieldname.verticalspeed=Pystysuora nopeus
 fieldname.description=Kuvaus
+fieldname.comment=Kommentti
 fieldname.mediafilename=Median tiedostonimi
 
 # Measurement units
@@ -834,8 +838,6 @@ error.language.wrongfile=Valittu tiedosto ei n\u00e4yt\u00e4 olevan GpsPrune:n k
 error.convertnamestotimes.nonames=Kohdepisteiden nimi\u00e4 ei voitu muuntaa ajoiksi
 error.lookupsrtm.nonefound=N\u00e4iss\u00e4 pisteiss\u00e4 ei ole korkeustietoja
 error.lookupsrtm.nonerequired=Kaikilla pisteill\u00e4 on jo korkeustieto,\njoten ei ole mit\u00e4\u00e4n haettavaa
-error.gpsies.uploadnotok='gpsies'-palvelin palautti viestin:
-error.gpsies.uploadfailed=Tiedoston uppaus ep\u00e4onnistui virheilmoitukseen:
 error.showphoto.failed=Kuvan lataus ei onnistunut
 error.playaudiofailed=\u00c4\u00e4nileikkeen toisto ei onnistunut
 error.cache.notthere=Karttapalojen v\u00e4limuistihakemistoa ei l\u00f6ytynyt
index 9b3efecaf1750b17715a8834bc4c239276976b8c..2d293929d70aea06341e963f006d101d29cc800e 100644 (file)
@@ -878,8 +878,6 @@ error.language.wrongfile=Le fichier s\u00e9lectionn\u00e9 n'est pas un fichier d
 error.convertnamestotimes.nonames=Aucun nom n'a pu \u00eatre converti en horaire
 error.lookupsrtm.nonefound=Aucune valeur d'altitude trouv\u00e9e pour les points
 error.lookupsrtm.nonerequired=Tous les points ont d\u00e9j\u00e0 une altitude, il n'y a rien \u00e0 r\u00e9cup\u00e9rer
-error.gpsies.uploadnotok=Le serveur de Gpsies a renvoy\u00e9 le message
-error.gpsies.uploadfailed=L'envoi a \u00e9chou\u00e9 avec l'erreur
 error.showphoto.failed=Impossible de charger la photo
 error.playaudiofailed=\u00c9chec de la lecture du fichier audio
 error.cache.notthere=Le dossier du cache n'a pas \u00e9t\u00e9 trouv\u00e9
index 82398652143e853cf526ce5fd8f707e9cca556f7..c3e9b1b698c2546094a462c726f40286c53f06ea 100644 (file)
@@ -888,8 +888,6 @@ error.language.wrongfile=\u00dagy t\u0171nik, hogy a kiv\u00e1lasztott f\u00e1jl
 error.convertnamestotimes.nonames=A nevek nem konvert\u00e1lhat\u00f3k id\u0151adatokk\u00e1
 error.lookupsrtm.nonefound=Nem \u00e9rhet\u0151 el magass\u00e1gi \u00e9rt\u00e9k ezekhez a pontokhoz
 error.lookupsrtm.nonerequired=Az \u00f6sszes pont m\u00e1r rendelkezik magass\u00e1gadatokkal, \u00edgy nincs mit keresni
-error.gpsies.uploadnotok=A gpsies szerver a k\u00f6vetkez\u0151 \u00fczenetet adta vissza
-error.gpsies.uploadfailed=A felt\u00f6lt\u00e9s nem siker\u00fclt a k\u00f6vetkez\u0151 hib\u00e1val
 error.showphoto.failed=F\u00e9nyk\u00e9p bet\u00f6lt\u00e9se sikertelen
 error.playaudiofailed=A hangf\u00e1jl lej\u00e1tsz\u00e1sa nem siker\u00fclt
 error.cache.notthere=A csempegyors\u00edt\u00f3t\u00e1r k\u00f6nyvt\u00e1ra nem tal\u00e1lhat\u00f3
index 2e7b3aa860bea6ead5aff3ea9f3743dd7ec89c6b..38c46518b5e6434edd603a77d81ac40c050221eb 100644 (file)
@@ -882,8 +882,6 @@ error.language.wrongfile=Il file selezionato non sembra essere un file di lingua
 error.convertnamestotimes.nonames=Nomi non convertibili in orari
 error.lookupsrtm.nonefound=Valori di quota non trovati
 error.lookupsrtm.nonerequired=Tutti i punti hanno gi\u00e0 una quota, non c'\u00e8 niente da cercare
-error.gpsies.uploadnotok=Il server Gpsies ha riportato il messaggio
-error.gpsies.uploadfailed=Il caricamento \u00e8 fallito con l'errore
 error.showphoto.failed=Caricamento foto fallito
 error.playaudiofailed=Ripresa audio non riprodotta
 error.cache.notthere=Directory del cache di tasselli non trovato
index f698abae427d72739fc344164a11deec6ab2dcce..28a95843468de003ef6e9917852e6046291ed7d1 100644 (file)
@@ -622,6 +622,4 @@ error.language.wrongfile=\u9078\u629e\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb\
 error.convertnamestotimes.nonames=\u3069\u306e\u540d\u524d\u3082\u6642\u9593\u306b\u5909\u63db\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
 error.lookupsrtm.nonefound=\u3069\u306e\u6a19\u9ad8\u5024\u3082\u3053\u308c\u3089\u306e\u30dd\u30a4\u30f3\u30c8\u304b\u3089\u53d6\u5f97\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002
 error.lookupsrtm.nonerequired=\u5168\u3066\u306e\u30dd\u30a4\u30f3\u30c8\u306f\u6a19\u9ad8\u5024\u3092\u6301\u3063\u3066\u3044\u308b\u305f\u3081\u3001\u691c\u7d22\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002
-error.gpsies.uploadnotok=Gpsies\u30b5\u30fc\u30d0\u30fc\u304c\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u8fd4\u3057\u307e\u3057\u305f\u3002
-error.gpsies.uploadfailed=\u30a8\u30e9\u30fc\u306e\u305f\u3081\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f
 error.playaudiofailed=\u30aa\u30fc\u30c7\u30a3\u30aa\u30d5\u30a1\u30a4\u30eb\u306e\u518d\u751f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002
index 75332e4df5b1046944c47f33af852838b144f37e..d553df0f4a82f25829ce76374ba6cd1419476522 100644 (file)
@@ -626,6 +626,4 @@ error.language.wrongfile=\uc120\ud0dd\ud558\uc2e0 \ud30c\uc77c\uc774 GpsPrune\uc
 error.convertnamestotimes.nonames=\uc774\ub984\uc744 \uc2dc\uac04\uc73c\ub85c \ubcc0\ud658 \ud560 \uc218 \uc5c6\uc5b4\uc694./n(\uacbd\uc720\uc9c0\uc774\ub984\uc744 \ucc3e\uc9c0 \ubabb\ud588\uac70\ub098 \ubcc0\ud658\ud560 \uc218 \uc5c6\ub294 \uacbd\uc6b0\uc5d0\uc694.)
 error.lookupsrtm.nonefound=\uc774 \uc9c0\uc810\ub4e4\uc5d0\uc11c \uace0\ub3c4\uac12\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc5b4\uc694./n\uc774 \uc601\uc5ed\uc5d0\uc11c \ud0c0\uc77c\uc774 \uc5c6\uac70\ub098, \uc790\ub8cc\uac00 \uc54c\uc218 \uc5c6\ub294 \uac83\ub4e4\uc744 \ud3ec\ud568\ud558\uace0 \uc788\ub294 \uacbd\uc720.
 error.lookupsrtm.nonerequired=\ubaa8\ub4e0 \uc9c0\uc810\uc774 \uace0\ub3c4 \uc815\ubcf4\uac00 \uc788\uc5b4\uc11c, \ucc3e\uc744\uac8c \uc544\ubb34\uac83\ub3c4 \uc5c6\ub124\uc694.
-error.gpsies.uploadnotok=gpsies \uc11c\ubc84\uac00 \uba54\uc138\uc9c0\ub97c \ub2e4\uc74c\uacfc \uac19\uc740 \ub3cc\ub824\uc90d\ub2c8\ub2e4
-error.gpsies.uploadfailed=\ub2e4\uc74c\uacfc \uac19\uc740 \uc774\uc720\ub85c \uc5c5\ub85c\ub4dc\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4
 error.playaudiofailed=\uc18c\ub9ac\ud30c\uc77c \uc7ac\uc0dd \uc2e4\ud328
index c757ccf44d3fcad74f61adc11cf2a174a8fdb9cb..009d2488cc268dfa456dd8b57b1de767310525ae 100644 (file)
@@ -885,8 +885,6 @@ error.language.wrongfile=Het geselecteerde bestand is vermoedelijk geen taalbest
 error.convertnamestotimes.nonames=Namen konden niet naar tijden worden geconverteerd
 error.lookupsrtm.nonefound=Geen hoogtewaarden beschikbaar voor deze punten
 error.lookupsrtm.nonerequired=Alle punten hebben reeds hoogte, er hoeft niets te worden opgezocht.
-error.gpsies.uploadnotok=Gpsies server antwoordde met
-error.gpsies.uploadfailed=De upload is mislukt. Fout
 error.showphoto.failed=Foto laden mislukt
 error.playaudiofailed=Kon audiobestand niet afspelen
 error.cache.notthere=De tegelcache map niet gevonden
index 75bfc804d42f85ecff6cb508491c50a30d894364..2733749ebe7c2411c31c212ebd14f4383ad3177a 100644 (file)
@@ -97,6 +97,7 @@ function.convertnamestotimes=Zamie\u0144 nazwy punkt\u00f3w na czas
 function.deletefieldvalues=Usu\u0144 warto\u015bci
 function.findwaypoint=Znajd\u017a punkt po\u015bredni
 function.pastecoordinates=Wprowad\u017a nowe wsp\u00f3\u0142rz\u0119dne
+function.enterpluscode=Wpisz kod plus
 function.charts=Wykres
 function.show3d=Poka\u017c model 3D
 function.distances=Odleg\u0142o\u015bci
@@ -117,6 +118,7 @@ function.searchopencachingde=Szukaj w OpenCaching.de
 function.mapillary=Szukaj zdj\u0119cia w Mapillary
 function.downloadosm=Za\u0142aduj dane obszaru z OSM
 function.duplicatepoint=Duplikuj plik
+function.projectpoint=Rzucaj plik
 function.setcolours=Ustaw kolory
 function.setdisplaysettings=Ustawienia wy\u015bwietlacza
 function.setlanguage=Zmie\u0144 j\u0119zyk
@@ -369,6 +371,8 @@ dialog.wikipedia.column.distance=Odleg\u0142o\u015b\u0107
 dialog.wikipedia.nonefound=Brak wpis\u00f3w w wikipedii
 dialog.wikipedia.gallery=Galeria
 dialog.osmpois.column.name=Nazwa
+dialog.osmpois.column.type=Typ
+dialog.osmpois.nonefound=Nie znaleziono punkt\u00f3w
 dialog.geocaching.nonefound=Nic nie zosta\u0142o znalezione
 dialog.correlate.notimestamps=Punkty nie maj\u0105 znacznik\u00f3w czasu, nie mo\u017cna ich powi\u0105za\u0107 ze zdj\u0119ciami.
 dialog.correlate.nouncorrelatedphotos=Nie ma nie powi\u0105zanych zdj\u0119\u0107.\nCzy na pewno chcesz kontynuowa\u0107?
@@ -525,11 +529,16 @@ dialog.diskcache.deleteall=Usu\u0144 wszystkie p\u0142ytki
 dialog.diskcache.deleted=Usuni\u0119to %d plik\u00f3w z kesza
 dialog.deletefieldvalues.intro=Wybierz pola do skasowania z wybranego zakresu
 dialog.deletefieldvalues.nofields=Brak p\u00f3l do skasowania dla tego zakresu
+dialog.displaysettings.waypointicons=Ikony dla punkt\u00f3w
+dialog.displaysettings.wpicon.plectrum=Plektron
+dialog.displaysettings.wpicon.ring=Pier\u015Bcie\u0144
 dialog.displaysettings.linewidth=Wprowad\u017a grubo\u015b\u0107 linii do rysowania \u015bcie\u017cek
 dialog.displaysettings.antialias=U\u017Cyj antyaliasingu
 dialog.displaysettings.size.small=Ma\u0142e
 dialog.displaysettings.size.medium=\u015arednie
 dialog.displaysettings.size.large=Du\u017ce
+dialog.displaysettings.windowstyle=Styl okna (wymaga to restartu)
+dialog.displaysettings.windowstyle.default=Domy\u015Blny
 dialog.downloadosm.desc=Potwierd\u017a \u015bci\u0105gni\u0119cie danych dla tego obszaru z OSM:
 dialog.searchwikipedianames.search=Szukaj
 dialog.weather.location=Pozycja
@@ -561,12 +570,14 @@ dialog.deletebydate.column.keep=Zostaw
 dialog.deletebydate.column.delete=Usu\u0144
 dialog.setaltitudetolerance.text.metres=Limit (w metrach) poni\u017cej kt\u00f3rego, ma\u0142e spadki wzniosy b\u0119d\u0105 ignorowane
 dialog.setaltitudetolerance.text.feet=Limit (w stopach) poni\u017cej kt\u00f3rego, ma\u0142e spadki wzniosy b\u0119d\u0105 ignorowane
+dialog.settimezone.system=Stref\u0119 czasow\u0105 systemu
 dialog.settimezone.selectedzone=Wybrana strefa czasowa
 dialog.autoplay.duration=Czas trwania (sek)
 dialog.autoplay.usetimestamps=U\u017Cyj znacznik\u00f3w czasowych
 dialog.autoplay.rewind=Przewin\u0105\u0107
 dialog.autoplay.pause=Pauza
 dialog.autoplay.play=Graj
+dialog.projectpoint.bearing=Kierunek (stopnie od p\u00f3\u0142nocy)
 
 # 3d window
 dialog.3d.title=GpsPrune widok tr\u00f3jwymiarowy
@@ -831,6 +842,7 @@ error.load.nopoints=Nie znaleziono informacji o wsp\u00f3\u0142rz\u0119dnych w p
 error.load.unknownxml=Nieznany format xml:
 error.load.noxmlinzip=Nie znaleziono pliku xml w pliku zip
 error.load.othererror=B\u0142\u0105d czytania pliku:
+error.load.nopointsintext=Nie znaleziono wsp\u00f3\u0142rz\u0119dnych
 error.jpegload.dialogtitle=B\u0142\u0105d \u0142adowania zdj\u0119cia
 error.jpegload.nofilesfound=Nie znaleziono plik\u00f3w
 error.jpegload.nojpegsfound=Nie znaleziono plik\u00f3w jpeg
@@ -851,8 +863,6 @@ error.language.wrongfile=Wybrany plik nie jest plikiem z t\u0142umaczeniem dla G
 error.convertnamestotimes.nonames=\u017badne nazwy nie mog\u0142y zosta\u0107 zmienione na czas
 error.lookupsrtm.nonefound=Nie znaleziono danych o wysoko\u015bci.
 error.lookupsrtm.nonerequired=Wszystkie pola maj\u0105 informacj\u0119 o wysoko\u015bci, nie ma czego szuka\u0107
-error.gpsies.uploadnotok=Serwer Gpsies zwr\u00f3ci\u0142 informacj\u0119
-error.gpsies.uploadfailed=B\u0142\u0105d wysy\u0142ania
 error.showphoto.failed=Nie powiod\u0142o si\u0119 za\u0142adowanie zdj\u0119cia
 error.playaudiofailed=Nie powiod\u0142o si\u0119 odtwarzanie pliku audio
 error.cache.notthere=Nie znaleziono katalogu kesza
index 82fc0872e976f5d22b8bb9eec4f5ab1330ad92ef..8190965e8de708c991594d14756194286df112e8 100644 (file)
@@ -96,6 +96,7 @@ function.convertnamestotimes=Converter nomes dos pontos para tempos
 function.deletefieldvalues=Remover valores do campo
 function.findwaypoint=Encontrar ponto
 function.pastecoordinates=Inserir novas coordenadas
+function.enterpluscode=Inserir um plus code
 function.charts=Gr\u00e1ficos
 function.show3d=Visualizar 3D
 function.distances=Dist\u00e2ncias
@@ -821,8 +822,6 @@ error.language.wrongfile=O arquivo selecionado n\u00e3o parece ser um arquivo de
 error.convertnamestotimes.nonames=Nenhum nome pode ser convertido para tempo
 error.lookupsrtm.nonefound=Nenhum valor de altitude encontrado
 error.lookupsrtm.nonerequired=Todos os pontos j\u00e1 possuem altitude, assim n\u00e3o h\u00e1 nada a procurar
-error.gpsies.uploadnotok=O servidor Gpsies retornou a mensagem
-error.gpsies.uploadfailed=O envio falhou com o erro
 error.showphoto.failed=Falha ao carregar foto
 error.playaudiofailed=Falha ao reproduzir arquivo de \u00e1udio
 error.cache.notthere=A paste de cache de fundos n\u00e3o foi encontrada
index e3a5610410c74c72104902c01ae485f7533c14b3..7affd24348c668cd6d446e7e83ab0fa0ed93814f 100644 (file)
@@ -851,8 +851,6 @@ error.language.wrongfile=Fi\u0219ierul selectat nu pare a fi un fi\u0219ier de l
 error.convertnamestotimes.nonames=Niciun nume de waypoint nu a fost g\u0103sit sau nu au putut fi convertite
 error.lookupsrtm.nonefound=Pentru aceste puncte nu exist\u0103 valori de altitudine
 error.lookupsrtm.nonerequired=Toate punctele au deja altitudine, nu e nimic de calculat
-error.gpsies.uploadnotok=Server-ul gpsies a \u00eentors mesajul
-error.gpsies.uploadfailed=Upload-ul a e\u0219uat cu eroarea
 error.showphoto.failed=\u00cenc\u0103rcarea foto a e\u0219uat
 error.playaudiofailed=\u00cencercarea de a reda clipul audio a e\u0219uat
 error.cache.notthere=Directorul tile cache nu a fost g\u0103sit
index b1fe18064303b417686f4040b450765eada2384f..82a8fa9a33797948ca2af3e746a522d060bf429d 100644 (file)
@@ -883,8 +883,6 @@ error.language.wrongfile=\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439
 error.convertnamestotimes.nonames=\u041d\u0435\u0442 \u0438\u043c\u0435\u043d, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u0432\u043e \u0432\u0440\u0435\u043c\u044f
 error.lookupsrtm.nonefound=\u041d\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043e\u0442\u043c\u0435\u0442\u043e\u043a \u0432\u044b\u0441\u043e\u0442 \u0434\u043b\u044f \u044d\u0442\u0438\u0445 \u0442\u043e\u0447\u0435\u043a.
 error.lookupsrtm.nonerequired=\u0412\u0441\u0435 \u0442\u043e\u0447\u043a\u0438 \u0443\u0436\u0435 \u0438\u043c\u0435\u044e\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432\u044b\u0441\u043e\u0442, \u043d\u0435\u0447\u0435\u0433\u043e \u043f\u0440\u043e\u0441\u043c\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044c
-error.gpsies.uploadnotok=Gpsies \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435
-error.gpsies.uploadfailed=\u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0430 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439
 error.showphoto.failed=\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u043e\u0442\u043e
 error.playaudiofailed=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u0437\u0432\u0443\u043a\u043e\u0437\u0430\u043f\u0438\u0441\u0438
 error.cache.notthere=\u041f\u0430\u043f\u043a\u0430 \u043a\u044d\u0448\u0430 \u0441 \u0442\u0430\u0439\u043b\u0430\u043c\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430
index 6192c5ce5dbefb2fc359bfea668758a591cebb21..4e46980887292f702f79aeacb91a7a23fafbb165 100644 (file)
@@ -167,6 +167,7 @@ dialog.openoptions.filesnippet=Filextrakt
 dialog.load.table.field=F\u00e4lt
 dialog.load.table.datatype=Datatyp
 dialog.load.table.description=Beskrivning
+dialog.delimiter.label=Avgr\u00e4nsare
 dialog.delimiter.comma=Komma ,
 dialog.delimiter.tab=Tabb
 dialog.delimiter.space=Mellanslag
@@ -715,6 +716,7 @@ display.range.time.days=d
 details.range.avespeed=Medelhastighet
 details.range.maxspeed=Maxhastighet
 details.range.numsegments=Antal segment
+details.range.pace=Tempo
 details.range.gradient=Gradient
 details.lists.waypoints=Waypoints
 details.lists.photos=Foton
@@ -742,8 +744,10 @@ fieldname.date=Datum
 fieldname.waypointname=Namn
 fieldname.waypointtype=Typ
 fieldname.newsegment=Segment
+fieldname.custom=Anpassad
 fieldname.prefix=F\u00e4lt
 fieldname.distance=Distans
+fieldname.duration=Varaktighet
 fieldname.speed=Hastighet
 fieldname.verticalspeed=Vertikal hastighet
 fieldname.description=Beskrivning
@@ -855,3 +859,5 @@ error.jpegload.nogpsfound=Ingen GPS-information hittades
 error.audioload.nofilesfound=Inga ljud-klipp hittades
 error.gpsload.unknown=Ok\u00e4nt fel
 error.undofailed.title=\u00c5ngra misslyckades
+error.function.notavailable.title=Funktionen \u00e4r inte tillg\u00e4nglig
+error.readme.notfound=Ingen L\u00e4sMig-fil hittades
index d915629508ca5685d584579a7f57ff69ed76a03a..fb68fb23358491e03e3f9dc4722792d8cc796606 100644 (file)
@@ -821,8 +821,6 @@ error.language.wrongfile=\u6240\u9009\u8bed\u8a00\u5305\u6587\u4ef6\u683c\u5f0f\
 error.convertnamestotimes.nonames=\u540d\u79f0\u65e0\u6cd5\u8f6c\u6362\u6210\u65f6\u95f4
 error.lookupsrtm.nonefound=\u65e0\u9ad8\u5ea6\u4fe1\u606f
 error.lookupsrtm.nonerequired=\u6240\u6709\u70b9\u5747\u542b\u9ad8\u5ea6\u4fe1\u606f
-error.gpsies.uploadnotok=gpsies\u670d\u52a1\u5668\u8fd4\u56de\u4fe1\u606f\uff1a
-error.gpsies.uploadfailed=\u4e0a\u4f20\u5931\u8d25
 error.showphoto.failed=\u52a0\u8f7d\u7167\u7247\u5931\u8d25
 error.playaudiofailed=\u65e0\u6cd5\u64ad\u653e\u58f0\u97f3\u6587\u4ef6
 error.cache.notthere=\u672a\u627e\u5230\u533a\u57df\u6570\u636e\u7f13\u5b58\u6587\u4ef6\u5939
index 5c54dfb5f574cf2d33a676fec30d1c93c33c1d06..73c40ac8492ff782a0b8d92dc97c81996b3c111c 100644 (file)
@@ -100,6 +100,7 @@ public abstract class BabelFileFormats
                        "GeocachingDB for Palm/OS", "gcdb", null,
                        "Geogrid-Viewer ascii overlay file", "ggv_ovl", ".ovl",
                        "Geogrid-Viewer tracklogs", "ggv_log", ".log",
+                       "GeoJSON", "geojson", ".json",
                        "GEOnet Names Server (GNS)", "geonet", null,
                        "GlobalSat DG-100/BT-335", "dg-100", null,
                        "GlobalSat DG-200", "dg-200", null,
index 52bdef232942f0c5290d2c8160dccba45ce0ef92..ba1100581e90d2c371e28017eef7ad6386a100fb 100644 (file)
@@ -10,6 +10,7 @@ import javax.swing.JFrame;
 import tim.prune.App;
 import tim.prune.config.Config;
 import tim.prune.data.Photo;
+import tim.prune.load.json.JsonFileLoader;
 import tim.prune.load.xml.GzipFileLoader;
 import tim.prune.load.xml.XmlFileLoader;
 import tim.prune.load.xml.ZipFileLoader;
@@ -142,6 +143,10 @@ public class FileLoader
                        _app.informPhotosLoaded(photoSet);
                        _app.informNoDataLoaded(); // To trigger load of next file if any
                }
+               else if (fileExtension.equals("json"))
+               {
+                       new JsonFileLoader(_app).openFile(inFile);
+               }
                else
                {
                        // Use text loader for everything else
diff --git a/src/tim/prune/load/json/JsonBlock.java b/src/tim/prune/load/json/JsonBlock.java
new file mode 100644 (file)
index 0000000..83486c7
--- /dev/null
@@ -0,0 +1,227 @@
+package tim.prune.load.json;
+
+import java.util.ArrayList;
+
+/**
+ * Structure to hold the contents of a Json block during parsing.
+ * This will be held within the current [] or {} block
+ */
+public class JsonBlock
+{
+       private String _latitude = null;
+       private String _longitude = null;
+       private String _altitude = null;
+
+       private Expectation _nextField = Expectation.NONE;
+       private BlockType _type = BlockType.NONE;
+       private boolean _hasNonNumbers = false;
+       private ArrayList<String> _pointCoords = null;
+       private ArrayList<ArrayList<String>> _coordList = null;
+
+
+       private enum BlockType {
+               NONE, FIELDS, POINTCOORDS, ISPOINTLIST, HASPOINTLIST
+       }
+       private enum Expectation {
+               NONE, LATITUDE, LONGITUDE, ALTITUDE, COORDS
+       }
+
+       /** Internal method to remove quotes and NaNs from altitude strings */
+       private String modifyAltitudeString(String inAlt)
+       {
+               if (inAlt == null || inAlt.equals("") || inAlt.equals("\"\"")) {
+                       return null;
+               }
+               String result = inAlt;
+               if (inAlt.length() > 2 && inAlt.startsWith("\"") && inAlt.endsWith("\""))
+               {
+                       result = inAlt.substring(1, inAlt.length()-1);
+               }
+               if (result.equals("NaN")) {return null;}
+               return result;
+       }
+
+       /**
+        * Receive a token to this block
+        * @param inToken token from Json source
+        */
+       public void addToken(String inToken)
+       {
+               if (inToken == null || inToken.isEmpty()) {return;}
+               if (!_hasNonNumbers && !looksLikeNumber(inToken)) {
+                       _hasNonNumbers = true;
+               }
+               if (inToken.equals("\"latitude\"")) {
+                       _nextField = Expectation.LATITUDE;
+               }
+               else if (inToken.equals("\"longitude\"")) {
+                       _nextField = Expectation.LONGITUDE;
+               }
+               else if (inToken.equals("\"altitude\"")) {
+                       _nextField = Expectation.ALTITUDE;
+               }
+               else if (inToken.equals("\"coordinates\"")) {
+                       _nextField = Expectation.COORDS;
+               }
+               else
+               {
+                       switch (_nextField)
+                       {
+                               case LATITUDE:
+                                       _latitude = inToken;
+                                       _type = BlockType.FIELDS;
+                                       break;
+                               case LONGITUDE:
+                                       _longitude = inToken;
+                                       _type = BlockType.FIELDS;
+                                       break;
+                               case ALTITUDE:
+                                       _altitude = modifyAltitudeString(inToken);
+                                       _type = BlockType.FIELDS;
+                                       break;
+                               default:
+                                       if (!_hasNonNumbers && looksLikeNumber(inToken))
+                                       {
+                                               if (_pointCoords == null) {
+                                                       _pointCoords = new ArrayList<String>();
+                                               }
+                                               _pointCoords.add(inToken);
+                                               _type = BlockType.POINTCOORDS;
+                                       }
+                                       break;
+                       }
+                       _nextField = Expectation.NONE;
+               }
+       }
+
+       /** @return true if String can be parsed as a double */
+       private static boolean looksLikeNumber(String inToken)
+       {
+               double value = Double.NaN;
+               try {
+                       value = Double.parseDouble(inToken);
+               }
+               catch (NumberFormatException nfe) {}
+               return !Double.isNaN(value);
+       }
+
+       /** @return true if named fields are valid */
+       public boolean areFieldsValid() {
+               return _type == BlockType.FIELDS && _latitude != null && _longitude != null;
+       }
+
+       /** @return true if list of 2 or 3 doubles for a single point is valid */
+       public boolean areSingleCoordsValid()
+       {
+               return !_hasNonNumbers && _pointCoords != null
+                       && _pointCoords.size() >= 2 && _pointCoords.size() <= 3;
+       }
+
+       /**
+        * @param inNewSegment new segment flag
+        * @return created point
+        */
+       public JsonPoint createSinglePoint(boolean inNewSegment)
+       {
+               return new JsonPoint(_latitude, _longitude, _altitude, inNewSegment);
+       }
+
+       /**
+        * Child block has finished processing a single set of point coordinates
+        * @param inChild child block
+        */
+       public void addSingleCoordsFrom(JsonBlock inChild)
+       {
+               if (_type == BlockType.NONE && _nextField == Expectation.COORDS
+                       && inChild._type == BlockType.POINTCOORDS)
+               {
+                       // Named coordinates field
+                       _type = BlockType.FIELDS;
+                       _longitude = inChild.getSinglePointCoords(0);
+                       _latitude = inChild.getSinglePointCoords(1);
+                       _altitude = inChild.getSinglePointCoords(2);
+               }
+               else if ((_type == BlockType.NONE || _type == BlockType.ISPOINTLIST)
+                       && !_hasNonNumbers && _nextField == Expectation.NONE
+                       && inChild._type == BlockType.POINTCOORDS)
+               {
+                       // add coordinates to list
+                       _type = BlockType.ISPOINTLIST;
+                       if (_coordList == null) {
+                               _coordList = new ArrayList<ArrayList<String>>();
+                       }
+                       _coordList.add(inChild._pointCoords);
+               }
+       }
+
+       /** @return point coordinate for given index, or null if not present */
+       private String getSinglePointCoords(int inIndex)
+       {
+               if (_pointCoords == null || inIndex < 0 || inIndex >= _pointCoords.size()) {
+                       return null;
+               }
+               return _pointCoords.get(inIndex);
+       }
+
+       /** @return true if list of point coords is valid */
+       public boolean isCoordListValid()
+       {
+               return !_hasNonNumbers && _type == BlockType.ISPOINTLIST
+                       && _coordList != null;
+       }
+
+       /** @return true if this block has a valid list of point coords */
+       public boolean hasValidCoordList()
+       {
+               return _type == BlockType.HASPOINTLIST && _coordList != null;
+       }
+
+       /**
+        * Child block has finished processing a list of point coordinates
+        * @param inChild child block
+        */
+       public void addCoordListFrom(JsonBlock inChild)
+       {
+               if (_type == BlockType.NONE && _nextField == Expectation.COORDS
+                       && inChild._type == BlockType.ISPOINTLIST)
+               {
+                       _coordList = inChild._coordList;
+                       _type = BlockType.HASPOINTLIST;
+               }
+               else if ((_type == BlockType.NONE || _type == BlockType.ISPOINTLIST)
+                       && !_hasNonNumbers && inChild._type == BlockType.ISPOINTLIST)
+               {
+                       if (_coordList == null) {
+                               _coordList = new ArrayList<ArrayList<String>>();
+                       }
+                       _coordList.addAll(inChild._coordList);
+                       _type = BlockType.ISPOINTLIST;
+               }
+       }
+
+       /**
+        * @return number of points in the list, if this block has a list
+        */
+       public int getNumPoints()
+       {
+               if (hasValidCoordList()) {
+                       return _coordList.size();
+               }
+               return 0;
+       }
+
+       /**
+        * @param inIndex point index within list
+        * @return created point for specified index
+        */
+       public JsonPoint createPointFromList(int inIndex)
+       {
+               if (inIndex < 0 || inIndex >= getNumPoints()) {return null;}
+               ArrayList<String> pointCoords = _coordList.get(inIndex);
+               if (pointCoords.size() < 2 || pointCoords.size() > 3) {return null;}
+               final String longitude = pointCoords.get(0);
+               final String latitude = pointCoords.get(1);
+               final String altitude = ((pointCoords.size() == 3) ? pointCoords.get(3) : null);
+               return new JsonPoint(latitude, longitude, altitude, inIndex == 0);
+       }
+}
diff --git a/src/tim/prune/load/json/JsonFileLoader.java b/src/tim/prune/load/json/JsonFileLoader.java
new file mode 100644 (file)
index 0000000..e1df666
--- /dev/null
@@ -0,0 +1,170 @@
+package tim.prune.load.json;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Stack;
+
+import tim.prune.App;
+import tim.prune.data.Field;
+import tim.prune.data.SourceInfo;
+
+
+/**
+ * Class to handle the loading of GeoJSON files
+ */
+public class JsonFileLoader
+{
+       /** App for callback of file loading */
+       private App _app = null;
+       /** Stack of blocks */
+       private Stack<JsonBlock> _jsonBlocks = null;
+       /** List of points extracted */
+       private ArrayList<JsonPoint> _jsonPoints = null;
+       private boolean _newSegment = true;
+
+
+       /**
+        * Constructor
+        * @param inApp App object
+        */
+       public JsonFileLoader(App inApp)
+       {
+               _app = inApp;
+               _jsonBlocks = new Stack<JsonBlock>();
+               _jsonPoints = new ArrayList<JsonPoint>();
+       }
+
+       /**
+        * Open the selected file
+        * @param inFile File to open
+        */
+       public void openFile(File inFile)
+       {
+               BufferedReader reader = null;
+               try
+               {
+                       reader = new BufferedReader(new FileReader(inFile));
+                       String currLine = reader.readLine();
+                       while (currLine != null)
+                       {
+                               processTokensInLine(currLine);
+                               // Read next line, if any
+                               currLine = reader.readLine();
+                       }
+               }
+               catch (IOException ioe) {
+                       _app.showErrorMessage("error.load.dialogtitle", "error.load.noread");
+               }
+               finally
+               {
+                       // close file ignoring errors
+                       try {
+                               if (reader != null) reader.close();
+                       }
+                       catch (Exception e) {}
+               }
+               if (_jsonPoints.size() > 0)
+               {
+                       Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE,
+                               Field.NEW_SEGMENT};
+                       _app.informDataLoaded(fields, makeDataArray(),
+                               null, new SourceInfo(inFile, SourceInfo.FILE_TYPE.JSON), null);
+               }
+               // TODO: Show message if nothing was found?
+       }
+
+       /** Split the given line from the json into tokens
+        *  and process them one by one */
+       private void processTokensInLine(String inLine)
+       {
+               if (inLine == null) {return;}
+               String line = inLine.strip();
+               StringBuilder currToken = new StringBuilder();
+               boolean insideQuotes = false;
+               boolean previousSlash = false;
+               for (char x : line.toCharArray())
+               {
+                       if (insideQuotes || x=='"') {
+                               currToken.append(x);
+                       }
+                       else
+                       {
+                               if (" :,".indexOf(x) >= 0) {
+                                       processToken(currToken.toString());
+                                       currToken.setLength(0);
+                               }
+                               else if ("[{".indexOf(x) >= 0) {
+                                       // start of a new block
+                                       _jsonBlocks.add(new JsonBlock());
+                               }
+                               else if ("]}".indexOf(x) >= 0)
+                               {
+                                       processToken(currToken.toString());
+                                       currToken.setLength(0);
+                                       // end of the current block
+                                       processBlock(_jsonBlocks.pop());
+                               }
+                               else {
+                                       currToken.append(x);
+                               }
+                       }
+                       if (x == '"' && !previousSlash) {insideQuotes = !insideQuotes;}
+                       previousSlash = (x == '\\') && !previousSlash;
+               }
+               processToken(currToken.toString());
+       }
+
+       private void processToken(String inToken)
+       {
+               if (inToken == null || inToken.isBlank()) {return;}
+               if (inToken.equals("\"coordinates\"")) {
+                       _newSegment = true;
+               }
+               _jsonBlocks.peek().addToken(inToken);
+       }
+
+       /** Process the end of the given block */
+       private void processBlock(JsonBlock inBlock)
+       {
+               if (inBlock.areFieldsValid())
+               {
+                       _jsonPoints.add(inBlock.createSinglePoint(_newSegment));
+                       _newSegment = false;
+               }
+               else if (inBlock.areSingleCoordsValid())
+               {
+                       // block contains a single point - pass to parent list
+                       _jsonBlocks.peek().addSingleCoordsFrom(inBlock);
+               }
+               else if (inBlock.isCoordListValid())
+               {
+                       // block contains a list of point coords
+                       _jsonBlocks.peek().addCoordListFrom(inBlock);
+               }
+               else if (inBlock.hasValidCoordList())
+               {
+                       for (int i=0; i<inBlock.getNumPoints(); i++)
+                       {
+                               _jsonPoints.add(inBlock.createPointFromList(i));
+                       }
+                       _newSegment = true;
+               }
+       }
+
+       /**
+        * Make an object array from the data list
+        * @return object array for loading
+        */
+       private Object[][] makeDataArray()
+       {
+               Object[][] result = new Object[_jsonPoints.size()][];
+               for (int i=0; i<_jsonPoints.size(); i++) {
+                       JsonPoint point = _jsonPoints.get(i);
+                       result[i] = new String[] {point._latitude, point._longitude, point._altitude, point._newSegment?"1":"0"};
+               }
+               return result;
+       }
+}
diff --git a/src/tim/prune/load/json/JsonPoint.java b/src/tim/prune/load/json/JsonPoint.java
new file mode 100644 (file)
index 0000000..a02bb15
--- /dev/null
@@ -0,0 +1,25 @@
+package tim.prune.load.json;
+
+/**
+ * Structure for holding a single point extracted from the Json
+ */
+public class JsonPoint
+{
+       public String _latitude = null, _longitude = null, _altitude = null;
+       public boolean _newSegment = false;
+
+       /**
+        * Constructor
+        * @param inLat latitude string
+        * @param inLon longitude string
+        * @param inAlt altitude string
+        * @param inNewSegment true if this point starts a new segment
+        */
+       public JsonPoint(String inLat, String inLon, String inAlt, boolean inNewSegment)
+       {
+               _latitude = inLat;
+               _longitude = inLon;
+               _altitude = inAlt;
+               _newSegment = inNewSegment;
+       }
+}
index f6efc99b4aa53a521c8ddf88cffeec98030f0a3b..2c30761268d941bad44eb0c810542ca40d7f6d5b 100644 (file)
@@ -1,4 +1,4 @@
-GpsPrune version 20.3
+GpsPrune version 20.4
 =====================
 
 GpsPrune is an application for viewing, editing and managing coordinate data from GPS systems,
@@ -17,7 +17,7 @@ Running
 =======
 
 To run GpsPrune from the jar file, simply call it from a command prompt or shell:
-   java -jar gpsprune_20.3.jar
+   java -jar gpsprune_20.4.jar
 
 If the jar file is saved in a different directory, you will need to include the path.
 Depending on your system settings, you may be able to click or double-click on the jar file
@@ -25,9 +25,16 @@ in a file manager window to execute it.  A shortcut, menu item, alias, desktop i
 or other link can of course be made should you wish.
 
 To specify a language other than the default, use an additional parameter, eg:
-   java -jar gpsprune_20.3.jar --lang=DE
+   java -jar gpsprune_20.4.jar --lang=DE
 
 
+New with version 20.4
+=====================
+The following fixes and additions were made since version 20.3:
+  - Loading of GeoJSON files, either directly or via GPSBabel
+  - Fix for calculations when some timestamps are missing (fperrin)
+  - Make 3d rotations more intuitive, avoid rotating upside-down (Issue #34)
+
 New with version 20.3
 =====================
 The following fixes and additions were made since version 20.2:
index f5b324390139f6f4af96271e19ce702a87ee52af..75c1c1d73021480ed0b18917dc15d1a85f9681d4 100644 (file)
@@ -54,7 +54,6 @@ import tim.prune.gui.map.MapSourceLibrary;
 import tim.prune.save.GroutedImage;
 import tim.prune.save.MapGrouter;
 
-import com.sun.j3d.utils.behaviors.vp.OrbitBehavior;
 import com.sun.j3d.utils.geometry.Box;
 import com.sun.j3d.utils.geometry.Cylinder;
 import com.sun.j3d.utils.geometry.GeometryInfo;
@@ -73,7 +72,7 @@ public class Java3DWindow implements ThreeDWindow
        private JFrame _parentFrame = null;
        private JFrame _frame = null;
        private ThreeDModel _model = null;
-       private OrbitBehavior _orbit = null;
+       private UprightOrbiter _orbit = null;
        private double _altFactor = -1.0;
        private ImageDefinition _imageDefinition = null;
        private GroutedImage _baseImage = null;
@@ -204,7 +203,7 @@ public class Java3DWindow implements ThreeDWindow
                u.getViewingPlatform().setNominalViewingTransform();
 
                // Add behaviour to rotate using mouse
-               _orbit = new OrbitBehavior(canvas, OrbitBehavior.REVERSE_ALL | OrbitBehavior.STOP_ZOOM);
+               _orbit = new UprightOrbiter(canvas, INITIAL_X_ROTATION);
                BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0);
                _orbit.setSchedulingBounds(bounds);
                u.getViewingPlatform().setViewPlatformBehavior(_orbit);
@@ -359,7 +358,6 @@ public class Java3DWindow implements ThreeDWindow
                                // Store this back in the cache, maybe we'll need it again
                                TerrainCache.storeTerrainTrack(terrainTrack, _dataStatus, _terrainDefinition);
                        }
-                       // else System.out.println("Yay - reusing the cached track!");
 
                        // Give the terrain definition to the _model as well
                        _model.setTerrain(terrainTrack);
diff --git a/src/tim/prune/threedee/UprightOrbiter.java b/src/tim/prune/threedee/UprightOrbiter.java
new file mode 100644 (file)
index 0000000..dd00ec7
--- /dev/null
@@ -0,0 +1,326 @@
+package tim.prune.threedee;
+
+import java.awt.event.InputEvent;
+
+/*
+ * Copyright (c) 2007 Sun Microsystems, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * This software is provided "AS IS," without a warranty of any
+ * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
+ * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
+ * EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
+ * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
+ * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
+ * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
+ * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
+ * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
+ * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
+ * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGES.
+ *
+ * You acknowledge that this software is not designed, licensed or
+ * intended for use in the design, construction, operation or
+ * maintenance of any nuclear facility.
+ * 
+ * Copyright (c) 2021 ActivityWorkshop simplifications and renamings,
+ * and restriction to upright orientations.
+ */
+
+import java.awt.event.MouseEvent;
+import java.awt.AWTEvent;
+
+import javax.media.j3d.Transform3D;
+import javax.media.j3d.Canvas3D;
+
+import javax.vecmath.Vector3d;
+import javax.vecmath.Point3d;
+import javax.vecmath.Matrix3d;
+
+import com.sun.j3d.utils.behaviors.vp.ViewPlatformAWTBehavior;
+import com.sun.j3d.utils.universe.ViewingPlatform;
+
+
+/**
+ * Moves the View around a point of interest when the mouse is dragged with
+ * a mouse button pressed.  Includes rotation, zoom, and translation
+ * actions. Zooming can also be obtained by using mouse wheel.
+ * <p>
+ * The rotate action rotates the ViewPlatform around the point of interest
+ * when the mouse is moved with the main mouse button pressed.  The
+ * rotation is in the direction of the mouse movement, with a default
+ * rotation of 0.01 radians for each pixel of mouse movement.
+ * <p>
+ * The zoom action moves the ViewPlatform closer to or further from the
+ * point of interest when the mouse is moved with the middle mouse button
+ * pressed (or Alt-main mouse button on systems without a middle mouse button).
+ * The default zoom action is to translate the ViewPlatform 0.01 units for each
+ * pixel of mouse movement.  Moving the mouse up moves the ViewPlatform closer,
+ * moving the mouse down moves the ViewPlatform further away.
+ * <p>
+ * The translate action translates the ViewPlatform when the mouse is moved
+ * with the right mouse button pressed.  The translation is in the direction
+ * of the mouse movement, with a default translation of 0.01 units for each
+ * pixel of mouse movement.
+ * <p>
+ * The actions can be reversed using the <code>REVERSE_</code><i>ACTION</i>
+ * constructor flags.  The default action moves the ViewPlatform around the
+ * objects in the scene.  The <code>REVERSE_</code><i>ACTION</i> flags can
+ * make the objects in the scene appear to be moving in the direction
+ * of the mouse movement.
+ */
+public class UprightOrbiter extends ViewPlatformAWTBehavior
+{
+       private Transform3D _longitudeTransform = new Transform3D();
+       private Transform3D _latitudeTransform = new Transform3D();
+       private Transform3D _rotateTransform = new Transform3D();
+
+       // needed for integrateTransforms but don't want to new every time
+       private Transform3D _temp1 = new Transform3D();
+       private Transform3D _temp2 = new Transform3D();
+       private Transform3D _translation = new Transform3D();
+       private Vector3d _transVector = new Vector3d();
+       private Vector3d _distanceVector = new Vector3d();
+       private Vector3d _centerVector = new Vector3d();
+       private Vector3d _invertCenterVector = new Vector3d();
+
+       private double _deltaYaw = 0.0, _deltaPitch = 0.0;
+       private double _startDistanceFromCenter = 20.0;
+       private double _distanceFromCenter = 20.0;
+       private Point3d _rotationCenter = new Point3d();
+       private Matrix3d _rotMatrix = new Matrix3d();
+
+       private int _mouseX = 0, _mouseY = 0;
+
+       private double _xtrans = 0.0, _ytrans = 0.0, _ztrans = 0.0;
+
+       private static final double MIN_RADIUS = 0.0;
+
+       // the factor to be applied to wheel zooming so that it does not 
+       // look much different with mouse movement zooming. 
+       private static final float wheelZoomFactor = 50.0f;
+
+       private static final double NOMINAL_ZOOM_FACTOR = .01;
+       private static final double NOMINAL_ROT_FACTOR = .008;
+       private static final double NOMINAL_TRANS_FACTOR = .003;
+
+       private double _pitchAngle = 0.0;
+
+
+       /**
+        * Creates a new OrbitBehaviour
+        * @param inCanvas The Canvas3D to add the behaviour to
+        * @param inInitialPitch pitch angle in degrees
+        */
+       public UprightOrbiter(Canvas3D inCanvas, double inInitialPitch)
+       {
+               super(inCanvas, MOUSE_LISTENER | MOUSE_MOTION_LISTENER | MOUSE_WHEEL_LISTENER );
+               _pitchAngle = Math.toRadians(inInitialPitch);
+       }
+
+       protected synchronized void processAWTEvents( final AWTEvent[] events )
+       {
+               motion = false;
+               for(int i=0; i<events.length; i++)
+                       if (events[i] instanceof MouseEvent) 
+                               processMouseEvent( (MouseEvent)events[i] );
+       }
+
+       protected void processMouseEvent( final MouseEvent evt )
+       {
+               if (evt.getID() == MouseEvent.MOUSE_PRESSED) {
+                       _mouseX = evt.getX();
+                       _mouseY = evt.getY();
+                       motion = true;
+               }
+               else if (evt.getID() == MouseEvent.MOUSE_DRAGGED)
+               {
+                       int xchange = evt.getX() - _mouseX;
+                       int ychange = evt.getY() - _mouseY;
+                       // rotate
+                       if (isRotateEvent(evt))
+                       {
+                               _deltaYaw -= xchange * NOMINAL_ROT_FACTOR;
+                               _deltaPitch -= ychange * NOMINAL_ROT_FACTOR;
+                       }
+                       // translate
+                       else if (isTranslateEvent(evt))
+                       {
+                               _xtrans -= xchange * NOMINAL_TRANS_FACTOR;
+                               _ytrans += ychange * NOMINAL_TRANS_FACTOR;
+                       }
+                       // zoom
+                       else if (isZoomEvent(evt)) {
+                               doZoomOperations( ychange );
+                       }
+                       _mouseX = evt.getX();
+                       _mouseY = evt.getY();
+                       motion = true;
+               }
+               else if (evt.getID() == MouseEvent.MOUSE_WHEEL )
+               {
+                       if (isZoomEvent(evt))
+                       {
+                               // if zooming is done through mouse wheel, the number of wheel increments
+                               // is multiplied by the wheelZoomFactor, to make zoom speed look natural
+                               if ( evt instanceof java.awt.event.MouseWheelEvent)
+                               {
+                                       int zoom = ((int)(((java.awt.event.MouseWheelEvent)evt).getWheelRotation()
+                                               * wheelZoomFactor));
+                                       doZoomOperations( zoom );
+                                       motion = true;
+                               }
+                       }
+               }
+       }
+
+       /*
+        * zoom but stop at MIN_RADIUS
+        */
+       private void doZoomOperations( int ychange )
+       {
+               if ((_distanceFromCenter - ychange * NOMINAL_ZOOM_FACTOR) > MIN_RADIUS) {
+                       _distanceFromCenter -= ychange * NOMINAL_ZOOM_FACTOR;
+               }
+               else {
+                       _distanceFromCenter = MIN_RADIUS;
+               }
+       }
+
+       /**
+        * Sets the ViewingPlatform for this behaviour.  This method is
+        * called by the ViewingPlatform.
+        * If a sub-calls overrides this method, it must call
+        * super.setViewingPlatform(vp).
+        * NOTE: Applications should <i>not</i> call this method.
+        */
+       @Override
+       public void setViewingPlatform(ViewingPlatform vp)
+       {
+               super.setViewingPlatform( vp );
+
+               if (vp != null) {
+                       resetView();
+                       integrateTransforms();
+               }
+       }
+
+       /**
+        * Reset the orientation and distance of this behaviour to the current
+        * values in the ViewPlatform Transform Group
+        */
+       private void resetView()
+       {
+               Vector3d centerToView = new Vector3d();
+
+               targetTG.getTransform( targetTransform );
+
+               targetTransform.get( _rotMatrix, _transVector );
+               centerToView.sub( _transVector, _rotationCenter );
+               _distanceFromCenter = centerToView.length();
+               _startDistanceFromCenter = _distanceFromCenter;
+
+               targetTransform.get( _rotMatrix );
+               _rotateTransform.set( _rotMatrix );
+
+               // compute the initial x/y/z offset
+               _temp1.set(centerToView);
+               _rotateTransform.invert();
+               _rotateTransform.mul(_temp1);
+               _rotateTransform.get(centerToView);
+               _xtrans = centerToView.x;
+               _ytrans = centerToView.y;
+               _ztrans = centerToView.z;
+
+               // reset rotMatrix
+               _rotateTransform.set( _rotMatrix );
+       }
+
+       protected synchronized void integrateTransforms()
+       {
+               // Check if the transform has been changed by another behaviour
+               Transform3D currentXfm = new Transform3D();
+               targetTG.getTransform(currentXfm);
+               if (! targetTransform.equals(currentXfm))
+                       resetView();
+
+               // Three-step rotation process, firstly undo the pitch and apply the delta yaw
+               _latitudeTransform.rotX(_pitchAngle);
+               _rotateTransform.mul(_rotateTransform, _latitudeTransform);
+               _longitudeTransform.rotY( _deltaYaw );
+               _rotateTransform.mul(_rotateTransform, _longitudeTransform);
+               // Now update pitch angle according to delta and apply
+               _pitchAngle = Math.min(Math.max(0.0, _pitchAngle - _deltaPitch), Math.PI/2.0);
+               _latitudeTransform.rotX(-_pitchAngle);
+               _rotateTransform.mul(_rotateTransform, _latitudeTransform);
+
+               _distanceVector.z = _distanceFromCenter - _startDistanceFromCenter;
+
+               _temp1.set(_distanceVector);
+               _temp1.mul(_rotateTransform, _temp1);
+
+               // want to look at rotationCenter
+               _transVector.x = _rotationCenter.x + _xtrans;
+               _transVector.y = _rotationCenter.y + _ytrans;
+               _transVector.z = _rotationCenter.z + _ztrans;
+
+               _translation.set(_transVector);
+               targetTransform.mul(_temp1, _translation);
+
+               // handle rotationCenter
+               _temp1.set(_centerVector);
+               _temp1.mul(targetTransform);
+
+               _invertCenterVector.x = -_centerVector.x;
+               _invertCenterVector.y = -_centerVector.y;
+               _invertCenterVector.z = -_centerVector.z;
+
+               _temp2.set(_invertCenterVector);
+               targetTransform.mul(_temp1, _temp2);
+
+               Vector3d finalTranslation = new Vector3d();
+               targetTransform.get(finalTranslation);
+
+               targetTG.setTransform(targetTransform);
+
+               // reset yaw and pitch deltas
+               _deltaYaw = 0.0;
+               _deltaPitch = 0.0;
+       }
+
+       private boolean isRotateEvent(MouseEvent evt)
+       {
+               final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0;
+               return !evt.isAltDown() && !isRightDrag;
+       }
+
+       private boolean isZoomEvent(MouseEvent evt)
+       {
+               if (evt instanceof java.awt.event.MouseWheelEvent) {
+                       return true;
+               }
+               return evt.isAltDown() && !evt.isMetaDown();
+       }
+
+       private boolean isTranslateEvent(MouseEvent evt)
+       {
+               final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0;
+               return !evt.isAltDown() && isRightDrag;
+       }
+}
diff --git a/test/tim/prune/data/RangeStatsTest.java b/test/tim/prune/data/RangeStatsTest.java
new file mode 100644 (file)
index 0000000..d068ac1
--- /dev/null
@@ -0,0 +1,197 @@
+package tim.prune.data;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for calculation of moving time of a range
+ * based on different timestamp availability
+ * @author fperrin
+ */
+class RangeStatsTest
+{
+       @Test
+       void movingTime()
+       {
+               Track track = new Track();
+               DataPoint[] points = {
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:00",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:05",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:07",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+               };
+               track.appendPoints(points);
+
+               RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+               assertEquals(7, range.getMovingDurationInSeconds());
+               assertEquals(7, range.getTotalDurationInSeconds());
+               assertFalse(range.getTimestampsIncomplete());
+               assertFalse(range.getTimestampsOutOfSequence());
+       }
+
+       @Test
+       void movingTimeWithGap()
+       {
+               Track track = new Track();
+               DataPoint[] points = {
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:00",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {},
+                               new FieldList(new Field[] {}),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:05",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:07",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+               };
+               track.appendPoints(points);
+
+               RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+               assertEquals(7, range.getMovingDurationInSeconds());
+               assertEquals(7, range.getTotalDurationInSeconds());
+               assertTrue(range.getTimestampsIncomplete());
+               assertFalse(range.getTimestampsOutOfSequence());
+       }
+
+       @Test
+       void movingTimeSeveralSegments()
+       {
+               Track track = new Track();
+               DataPoint[] points = {
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:01:00",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {},
+                               new FieldList(new Field[] {}),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:01:05",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:01:07",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       // start a second segment
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:20",
+                                       "1",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                                       Field.NEW_SEGMENT,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:27",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+               };
+               track.appendPoints(points);
+
+               RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+               assertEquals(7 + 7, range.getMovingDurationInSeconds());
+               assertEquals(47, range.getTotalDurationInSeconds());
+               assertTrue(range.getEarliestTimestamp().isEqual(new TimestampUtc("01-Jan-2020 00:00:20")));
+               assertTrue(range.getLatestTimestamp().isEqual(new TimestampUtc("01-Jan-2020 00:01:07")));
+               assertTrue(range.getTimestampsIncomplete());
+
+               // even though segment 2 is earlier than segment 1, timestamps
+               // within each segment are normally ordered
+               assertFalse(range.getTimestampsOutOfSequence());
+       }
+
+       @Test
+       void movingTimeMissingFirstTimestamp()
+       {
+               Track track = new Track();
+               DataPoint[] points = {
+                       new DataPoint(
+                               new String[] {},
+                               new FieldList(new Field[] {}),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:00",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+                       new DataPoint(
+                               new String[] {
+                                       "01-Jan-2020 00:00:05",
+                               },
+                               new FieldList(new Field[] {
+                                       Field.TIMESTAMP,
+                               }),
+                               null),
+               };
+               track.appendPoints(points);
+
+               RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+               assertEquals(5, range.getMovingDurationInSeconds());
+               assertEquals(5, range.getTotalDurationInSeconds());
+               assertTrue(range.getTimestampsIncomplete());
+               assertFalse(range.getTimestampsOutOfSequence());
+       }
+}
diff --git a/test/tim/prune/function/cache/TileSetTest.java b/test/tim/prune/function/cache/TileSetTest.java
new file mode 100644 (file)
index 0000000..4ff108c
--- /dev/null
@@ -0,0 +1,44 @@
+package tim.prune.function.cache;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for tile name checks
+ */
+class TileSetTest
+{
+       @Test
+       void testIsNumeric()
+       {
+               // not numeric, should be false
+               assertFalse(TileSet.isNumeric(null));
+               assertFalse(TileSet.isNumeric(""));
+               assertFalse(TileSet.isNumeric("a"));
+               assertFalse(TileSet.isNumeric(" "));
+               assertFalse(TileSet.isNumeric("155a"));
+               assertFalse(TileSet.isNumeric("-2"));
+               // numeric, should be true
+               assertTrue(TileSet.isNumeric("1"));
+               assertTrue(TileSet.isNumeric("155"));
+       }
+
+       @Test
+       void testIsNumericUntilDot()
+       {
+               // not numeric, should be false
+               assertFalse(TileSet.isNumericUntilDot(null));
+               assertFalse(TileSet.isNumericUntilDot(""));
+               assertFalse(TileSet.isNumericUntilDot("."));
+               assertFalse(TileSet.isNumericUntilDot(".abc"));
+               assertFalse(TileSet.isNumericUntilDot("a3."));
+               assertFalse(TileSet.isNumericUntilDot("4a"));
+               assertFalse(TileSet.isNumericUntilDot("215327h.png"));
+               // numeric but no dot, should be false
+               assertFalse(TileSet.isNumericUntilDot("1234"));
+               // numeric, should be true
+               System.out.println(TileSet.isNumericUntilDot("44.jpg"));
+               System.out.println(TileSet.isNumericUntilDot("0."));
+       }
+}
diff --git a/test/tim/prune/function/olc/OlcDecoderTest.java b/test/tim/prune/function/olc/OlcDecoderTest.java
new file mode 100644 (file)
index 0000000..951a081
--- /dev/null
@@ -0,0 +1,75 @@
+package tim.prune.function.olc;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for decoding of Open Location Codes (Pluscodes)
+ */
+class OlcDecoderTest
+{
+
+       @Test
+       void testDecodeStringsTooShort()
+       {
+               OlcArea area = OlcDecoder.decode(null);
+               assertEquals(area, null, "Decoding null gives null");
+               area = OlcDecoder.decode("");
+               assertEquals(area, null, "Decoding \"\" gives null");
+               area = OlcDecoder.decode("9");
+               assertEquals(area, null, "Decoding \"9\" gives null");
+               area = OlcDecoder.decode("9999999");
+               assertEquals(area, null, "Decoding \"9999999\" gives null");
+       }
+
+       @Test
+       void testDecodeStringsInvalid()
+       {
+               OlcArea area = OlcDecoder.decode("11111111");
+               assertEquals(area, null, "Decoding lots of 1s gives null");
+               area = OlcDecoder.decode("99999991");
+               assertEquals(area, null, "Decoding with a single 1 gives null");
+               area = OlcDecoder.decode("99999999");
+               assertNotEquals(area, null, "Decoding with all 9s gives non-null");
+               area = OlcDecoder.decode("00000000");
+               assertEquals(area, null, "Decoding with all padding gives null");
+               area = OlcDecoder.decode("99000000");
+               assertNotEquals(area, null, "Decoding with some padding gives non-null");
+       }
+
+       @Test
+       void testDecodeZeroes()
+       {
+               OlcArea area = OlcDecoder.decode("22000000");
+               assertNotEquals(area, null, "Decoding with padding gives non-null");
+               assertEquals(-90.0, area.minLat, 0.0, "South 90");
+               assertEquals(-70.0, area.maxLat, 0.0, "South 70");
+               assertEquals(-180.0, area.minLon, 0.0, "West 180");
+               assertEquals(-160.0, area.maxLon, 0.0, "West 160");
+       }
+
+       @Test
+       void testDecodeZeroes2()
+       {
+               OlcArea area = OlcDecoder.decode("22220000");
+               assertNotEquals(area, null, "Decoding with padding gives non-null");
+               assertEquals(-90.0, area.minLat, 0.0, "South 90");
+               assertEquals(-89.0, area.maxLat, 0.0, "South 89");
+               assertEquals(-180.0, area.minLon, 0.0, "West 180");
+               assertEquals(-179.0, area.maxLon, 0.0, "West 179");
+       }
+
+       @Test
+       void testMountainView()
+       {
+               OlcArea area = OlcDecoder.decode("6PH57VP3+PR6");
+               assertNotEquals(area, null, "Decoding with separator gives non-null");
+               System.out.println("Min lat: " + area.minLat);
+               System.out.println("Max lat: " + area.maxLat);
+               System.out.println("Min lon: " + area.minLon);
+               System.out.println("Max lon: " + area.maxLon);
+               assertTrue(area.maxLat > area.minLat, "latitude range");
+               assertTrue(area.maxLon > area.minLon, "longitude range");
+       }
+}
diff --git a/test/tim/prune/function/weather/SingleForecastTest.java b/test/tim/prune/function/weather/SingleForecastTest.java
new file mode 100644 (file)
index 0000000..372ab47
--- /dev/null
@@ -0,0 +1,52 @@
+package tim.prune.function.weather;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for weather icons
+ */
+class SingleForecastTest
+{
+
+       @Test
+       void testWeatherIcons()
+       {
+               testIconName(null, "100", "");
+               testIconName("storm.png", "200", null);
+               testIconName("storm.png", "204", "");
+               testIconName("lightrain.png", "300", null);
+               testIconName("lightrain.png", "301", null);
+               testIconName(null, "400", null);
+               testIconName("lightrain.png", "500", null);
+               testIconName("rain.png", "501", null);
+               testIconName("rain.png", "599", null);
+               testIconName("hail.png", "511", null);
+               testIconName("snow.png", "600", null);
+               testIconName("fog.png", "700", null);
+               testIconName("clear-day.png", "800", null);
+               testIconName("clear-day.png", "800", "");
+               testIconName("clear-day.png", "800", "01d");
+               testIconName("clear-night.png", "800", "01n");
+               testIconName("clouds-day.png", "802", "01d");
+               testIconName("clouds-night.png", "802", "01n");
+               testIconName("clouds.png", "804", "01n");
+               testIconName("extreme.png", "900", "01d");
+               testIconName("hail.png", "906", "01n");
+       }
+
+       /**
+        * Test getting an icon name according to code and image
+        */
+       private static void testIconName(String inExpect, String inCode, String inImage)
+       {
+               String icon = SingleForecast.getIconName(inCode, inImage);
+               assertEquals(inExpect, icon, showString(inCode) + ", " + showString(inImage));
+       }
+
+       private static String showString(String inString)
+       {
+               return inString == null ? "null" : inString;
+       }
+}
diff --git a/test/tim/prune/gui/map/MapSourceTest.java b/test/tim/prune/gui/map/MapSourceTest.java
new file mode 100644 (file)
index 0000000..ff947c3
--- /dev/null
@@ -0,0 +1,31 @@
+package tim.prune.gui.map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for manipulating base Urls
+ */
+class MapSourceTest
+{
+       @Test
+       void testFixBaseUrls()
+       {
+               // Should succeed
+               testUrlFix("8bitcities.s3.amazonaws.com", "http://8bitcities.s3.amazonaws.com/");
+               testUrlFix("8bitcities.s3.amazonaws.com/", "http://8bitcities.s3.amazonaws.com/");
+               testUrlFix("http://8bitcities.s3.amazonaws.com/", "http://8bitcities.s3.amazonaws.com/");
+               testUrlFix("something.com/ok", "http://something.com/ok/");
+
+               // These should fail and return null
+               testUrlFix("something/wrong", null);
+               testUrlFix("protocol://something.com/16/", null);
+       }
+
+       private void testUrlFix(String inStart, String inExpected)
+       {
+               String result = MapSource.fixBaseUrl(inStart);
+               assertEquals(inExpected, result);
+       }
+}
diff --git a/test/tim/prune/gui/map/SiteNameUtilsTest.java b/test/tim/prune/gui/map/SiteNameUtilsTest.java
new file mode 100644 (file)
index 0000000..e01ed52
--- /dev/null
@@ -0,0 +1,63 @@
+package tim.prune.gui.map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.HashSet;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for site name utils
+ */
+class SiteNameUtilsTest
+{
+
+       @Test
+       void testPickServerNameWithoutWildcards()
+       {
+               testPickSingleUrl("abc", "abc");
+               testPickSingleUrl("ab[]c", "abc");
+               testPickSingleUrl("[]abc", "abc");
+               testPickSingleUrl("abc[]", "abc");
+       }
+
+       /**
+        * Test a pattern without wildcards which should always produce the expected result
+        * @param inPattern pattern for site name
+        * @param inExpected expected resolved name
+        */
+       private void testPickSingleUrl(String inPattern, String inExpected)
+       {
+               for (int i=0; i<20; i++)
+               {
+                       String resolved = SiteNameUtils.pickServerUrl(inPattern);
+                       assertEquals(inExpected, resolved, "Failed: " + inPattern);
+               }
+       }
+
+       @Test
+       void testPickUsingWildcards()
+       {
+               testRandomPick("ab[123]c", new String[]{"ab1c", "ab2c", "ab3c"});
+               testRandomPick("1234.[abcd]", new String[]{"1234.a", "1234.b", "1234.c", "1234.d"});
+       }
+
+       /**
+        * Test a pattern with wildcards which should produce several different results randomly
+        * @param inPattern pattern for site name
+        * @param inExpected array of expected resolved names
+        */
+       private void testRandomPick(String inPattern, String[] inExpected)
+       {
+               HashSet<String> results = new HashSet<String>();
+               for (int i=0; i<30; i++)
+               {
+                       results.add(SiteNameUtils.pickServerUrl(inPattern));
+               }
+               // Check that all expected results were returned
+               assertEquals(inExpected.length, results.size());
+               for (String expec : inExpected) {
+                       assertTrue(results.contains(expec));
+               }
+       }
+}
diff --git a/test/tim/prune/jpeg/drew/RationalTest.java b/test/tim/prune/jpeg/drew/RationalTest.java
new file mode 100644 (file)
index 0000000..ae97e0f
--- /dev/null
@@ -0,0 +1,71 @@
+package tim.prune.jpeg.drew;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for the Rational values used by the Exif
+ */
+class RationalTest
+{
+       @Test
+       void testManyInts()
+       {
+               testIntVal(0, 0, 0);
+               testIntVal(1, 0, 0);
+               testIntVal(0, 1, 0);
+               for (int i=0; i<16000; i++)
+               {
+                       testIntVal(0, i, 0);
+                       testIntVal(i, 0, 0);
+                       testIntVal(i, 1, i);
+                       testIntVal(-i, 1, -i);
+                       testIntVal(i*2, 2, i);
+                       testIntVal(i*2+1, 2, i);        // rounding down the 0.5
+                       testIntVal(-i*2, 2, -i);
+                       testIntVal(i*2, -2, -i);
+                       testIntVal(-i*2, -2, i);
+               }
+       }
+
+       /**
+        * Check that a rational converts to an integer properly
+        * @param inTop number on top of the rational (numerator)
+        * @param inBottom number on bottom of the rational (denominator)
+        * @param inExpected expected int value
+        */
+       private void testIntVal(long inTop, long inBottom, int inExpected)
+       {
+               Rational value = new Rational(inTop, inBottom);
+               assertEquals(inExpected, value.intValue(), "" + inTop + "/" + inBottom);
+       }
+
+       @Test
+       void testManyDoubles()
+       {
+               for (int i=0; i<16000; i++)
+               {
+                       testDoubleVal(0, i, 0.0);
+                       testDoubleVal(i, 0, 0.0);
+                       testDoubleVal(i, 1, i);
+                       testDoubleVal(i*2, 2, i);
+                       testDoubleVal(i*2+1, 2, i+0.5);
+                       testDoubleVal(i*2, -2, -i);
+               }
+
+               testDoubleVal(123, 3, 123.0/3.0);
+       }
+
+       /**
+        * Check that a rational converts to a double properly
+        * @param inTop number on top of the rational (numerator)
+        * @param inBottom number on bottom of the rational (denominator)
+        * @param inExpected expected double value (exact)
+        */
+       private void testDoubleVal(long inTop, long inBottom, double inExpected)
+       {
+               Rational value = new Rational(inTop, inBottom);
+               assertEquals(inExpected, value.doubleValue(), 0.0, "" + inTop + "/" + inBottom);
+       }
+}