]> gitweb.fperrin.net Git - GpsPrune.git/commitdiff
Merge from 20.*
authoractivityworkshop <mail@activityworkshop.net>
Tue, 11 May 2021 19:05:18 +0000 (21:05 +0200)
committeractivityworkshop <mail@activityworkshop.net>
Tue, 11 May 2021 19:05:18 +0000 (21:05 +0200)
53 files changed:
README.md
buildtools/build.sh
buildtools/pom.xml
buildtools/version.properties
src/tim/prune/App.java
src/tim/prune/GpsPrune.java
src/tim/prune/data/RangeStats.java
src/tim/prune/function/AboutScreen.java
src/tim/prune/function/distance/DistanceFunction.java
src/tim/prune/function/srtm/LookupSrtmFunction.java
src/tim/prune/function/srtm/TileFinder.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/gui/map/TileDownloader.java
src/tim/prune/lang/prune-texts_af.properties
src/tim/prune/lang/prune-texts_cy.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/save/GpxExporter.java
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 94e10e0e2ed873e6bbf7558419de14e393c20f08..22fa4865b8ddb9ab5b7b3663a4ed2c4ce9c46587 100644 (file)
@@ -733,11 +733,11 @@ public class App
                                UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
                                undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
                                _undoStack.add(undo);
-                               _lastSavePosition = _undoStack.size();
                                _trackInfo.getSelection().clearAll();
                                _track.load(inLoadedTrack);
                                if (inSourceInfo != null)
                                {
+                                       _lastSavePosition = _undoStack.size();
                                        // set source information
                                        inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
                                        _trackInfo.getFileInfo().replaceSource(inSourceInfo);
@@ -752,11 +752,11 @@ public class App
                        UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
                        undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
                        _undoStack.add(undo);
-                       _lastSavePosition = _undoStack.size();
                        _trackInfo.getSelection().clearAll();
                        _track.load(inLoadedTrack);
                        if (inSourceInfo != null)
                        {
+                               _lastSavePosition = _undoStack.size();
                                inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
                                _trackInfo.getFileInfo().addSource(inSourceInfo);
                        }
index 5503ccf6e552e68b1698b0e3ca1ab2404c10cc9b..b4e3699dd9d02214e71e4f0459bab7a6a4cd896a 100644 (file)
@@ -31,7 +31,7 @@ import tim.prune.gui.profile.ProfileChart;
 /**
  * GpsPrune is a tool to visualize, edit, convert and prune GPS data
  * Please see the included readme.txt or https://activityworkshop.net
- * This software is copyright activityworkshop.net 2006-2020 and made available through the Gnu GPL version 2.
+ * This software is copyright activityworkshop.net 2006-2021 and made available through the Gnu GPL version 2.
  * For license details please see the included license.txt.
  * GpsPrune is the main entry point to the application, including initialisation and launch
  */
@@ -40,7 +40,7 @@ public class GpsPrune
        /** Version number of application, used in about screen and for version check */
        public static final String VERSION_NUMBER = "21";
        /** Build number, just used for about screen */
-       public static final String BUILD_NUMBER = "384";
+       public static final String BUILD_NUMBER = "388";
        /** 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 c8044b431cffccac56ae91d5f51d73f8411ca32e..6f98fcdb6b476a36bb002f3a67d4da153df56e7b 100644 (file)
@@ -97,7 +97,7 @@ public class AboutScreen extends GenericFunction
                descBuffer.append("<p>").append(I18nManager.getText("dialog.about.summarytext3")).append("</p>");
                descBuffer.append("<p>").append(I18nManager.getText("dialog.about.languages")).append(" : ")
                        .append("afrikaans, \u010de\u0161tina, deutsch, english, espa\u00F1ol, fran\u00E7ais, italiano,<br>" +
-                               " magyar, nederlands, polski, portugu\u00EAs, rom\u00E2n\u0103, suomi, \u0440\u0443\u0441\u0441\u043a\u0438\u0439 (russian),<br>" +
+                               " magyar, nederlands, polski, portugu\u00EAs, rom\u00E2n\u0103, suomi, svenska, \u0440\u0443\u0441\u0441\u043a\u0438\u0439 (russian),<br>" +
                                " \u4e2d\u6587 (chinese), \u65E5\u672C\u8A9E (japanese), \uD55C\uAD6D\uC5B4/\uC870\uC120\uB9D0 (korean), schwiizerd\u00FC\u00FCtsch</p>");
                descBuffer.append("<p>").append(I18nManager.getText("dialog.about.translatedby")).append("</p>");
                JEditorPane descPane = new JEditorPane("text/html", descBuffer.toString());
index e0b7aed7354938616982ba8d933f6e531f37d651..f37332420e5b662f2d472d4e76e1132725826cb3 100644 (file)
@@ -77,8 +77,9 @@ public class DistanceFunction extends GenericFunction
                }
                _fromModel.init(pointList);
                _distModel.init(pointList);
-               _pointTable.getSelectionModel().setSelectionInterval(0, 0);
-               _distModel.recalculate(0);
+               final int pointIndex = getPointIndex(pointList, _app.getTrackInfo());
+               _pointTable.getSelectionModel().setSelectionInterval(pointIndex, pointIndex);
+               _distModel.recalculate(pointIndex);
                _dialog.setVisible(true);
        }
 
@@ -163,4 +164,25 @@ public class DistanceFunction extends GenericFunction
                }
                return pointList;
        }
+
+       /**
+        * Find the point to select from the given point list
+        * @param pointList list of points
+        * @param inTrackInfo current track info to get selected point (if any)
+        * @return index of point to be selected
+        */
+       private static int getPointIndex(ArrayList<DataPoint> pointList, TrackInfo inTrackInfo)
+       {
+               DataPoint currPoint = inTrackInfo.getCurrentPoint();
+               if (currPoint != null && currPoint.isWaypoint())
+               {
+                       // Currently selected point is a waypoint, so select this one for convenience
+                       for (int i=0; i<pointList.size(); i++) {
+                               if (pointList.get(i) == currPoint) {
+                                       return i;
+                               }
+                       }
+               }
+               return 0;
+       }
 }
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 e2b01fa166dbd9b607f9e13890f0a12a7f5b7302..d237971eb05f38de1a2b8a7c7ae24f0ffa55e8fe 100644 (file)
@@ -15,7 +15,7 @@ public class TileFinder
        private byte[] _tileData = null;
 
        /** URL prefix for all tiles */
-       private static final String URL_PREFIX = "https://dds.cr.usgs.gov/srtm/version2_1/SRTM3/";
+       private static final String URL_PREFIX = "https://srtm.kurviger.de/SRTM3/";
        /** Directory names for each continent */
        private static final String[] CONTINENTS = {"", "Eurasia", "North_America", "Australia",
                "Islands", "South_America", "Africa"};
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 7ec6d91eb58db07faaa8c8a52421f5da5a8cf544..16466f3e8631b4051fca5415719972413df8ecb7 100644 (file)
@@ -63,16 +63,15 @@ public class TileDownloader implements Runnable
                if (inManager != null && inUrl != null)
                {
                        String url = inUrl.toString();
-                       // System.out.println("Trigger load: " + url);
-                       if (!BLOCKED_URLS.contains(url) && !LOADING_URLS.contains(url))
+                       if (BLOCKED_URLS.contains(url))
+                       {
+                               System.out.println("Already blocked: " + url);
+                       }
+                       else if (!LOADING_URLS.contains(url))
                        {
-                               // System.out.println("Not blocked: " + url);
                                LOADING_URLS.add(url);
                                new Thread(new TileDownloader(inManager, inUrl, inLayer, inX, inY, inZoom)).start();
                        }
-                       else {
-                               System.out.println("Already blocked: " + 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 50ef3a20e82baca7c89da57e4b9b2baf99b11688..12a2b17fe2d324696e6d0d8c43c2a63c26122067 100644 (file)
@@ -12,11 +12,16 @@ menu.photo=Llun
 menu.photo.saveexif=Cadw Exif
 menu.audio=Awdio
 menu.view=Golygu
+menu.view.browser.google=Mapiau Google
+menu.view.browser.yahoo=Mapiau Yahoo
+menu.view.browser.bing=Mapiau Bing
 menu.settings=Dewisiadau
 menu.help=Cymorth
 # Popup menu for map
 menu.map.zoomin=Chwyddo i mewn
 menu.map.zoomout=Chwyddo allan
+menu.map.showmap=Dangoswch y map
+menu.map.showscalebar=Dangoswch y bar graddfa
 
 # Alt keys for menus
 altkey.menu.file=F
@@ -48,6 +53,8 @@ dialog.delimiter.comma=Coma ,
 dialog.delimiter.semicolon=Hannercolon ;
 dialog.gpsload.format=Fformat
 dialog.gpsload.save=Cadw ffeil
+dialog.saveoptions.title=Cadwch y ffeil
+dialog.save.table.field=Maes
 dialog.save.table.save=Cadw
 dialog.exportgpx.name=Enw
 dialog.exportgpx.encoding.system=System
@@ -92,6 +99,8 @@ fieldname.longitude=Hydred
 fieldname.coordinates=Cyfesurynnau
 fieldname.waypointname=Enw
 fieldname.distance=Pellter
+fieldname.speed=Cyflymder
+fieldname.description=Disgrifiad
 fieldname.comment=Sylw
 
 # How to combine conditions, such as filters
index 1b49eb1e224b7c97b56f6af576ad3cc1fb8b462c..eca3590fe542c6ef274453dd609e3b4ad232c724 100644 (file)
@@ -1,5 +1,5 @@
 # Text entries for the GpsPrune application
-# Czech entries thanks to prot_d
+# Czech entries thanks to prot_d, jirislaby
 
 # Menu entries
 menu.file=Soubor
@@ -12,8 +12,6 @@ menu.track=Stopa
 menu.track.undo=Undo
 menu.track.clearundo=Vypr\u00e1zdnit pam\u011b\u0165 undo
 menu.track.markrectangle=Ozna\u010dit body v obd\u00e9ln\u00edku
-function.deletemarked=Smazat ozna\u010den\u00e9 body
-function.rearrangewaypoints=P\u0159euspo\u0159\u00e1dat z\u00e1jmov\u00e9 body
 menu.range=Rozmez\u00ed
 menu.range.all=Vybrat v\u0161e
 menu.range.none=Zru\u0161it v\u00fdb\u011br
@@ -39,7 +37,6 @@ menu.view.browser.yahoo=Mapy Yahoo
 menu.view.browser.bing=Mapy Bing
 menu.settings=Nastaven\u00ed
 menu.settings.onlinemode=Na\u010d\u00edtat mapy z internetu
-dialog.displaysettings.antialias=Pou\u017e\u00edt antialiasing
 menu.settings.autosave=P\u0159i ukon\u010den\u00ed automaticky ukl\u00e1dat
 menu.help=Pomoc
 # Popup menu for map
@@ -90,12 +87,15 @@ function.compress=Komprimovat stopu
 function.deleterange=Smazat rozmez\u00ed
 function.croptrack=O\u0159\u00edznout stopu
 function.interpolate=Interpolovat body
+function.deletebydate=Smazat body podle data
 function.addtimeoffset=P\u0159idat \u010dasov\u00fd posun
 function.addaltitudeoffset=P\u0159idat v\u00fd\u0161kov\u00fd posun
+function.rearrangewaypoints=P\u0159euspo\u0159\u00e1dat z\u00e1jmov\u00e9 body
 function.convertnamestotimes=P\u0159ev\u00e9st n\u00e1zvy v\u00fdzna\u010dn\u00fdch bod\u016f na \u010dasy
 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
@@ -103,6 +103,7 @@ function.estimatetime=Odhad \u010dasu
 function.learnestimationparams=Anal\u00fdza stopy pro odhad \u010dasu
 function.setmapbg=Nastavit pozad\u00ed
 function.setpaths=Nastavit cestu k program\u016fm
+function.selectsegment=Vybrat aktu\u00e1ln\u00ed \u010d\u00e1st
 function.splitsegments=Rozd\u011blit stopu na \u010d\u00e1sti
 function.sewsegments=Spojit \u010d\u00e1sti stopy
 function.lookupsrtm=Na\u010d\u00edst nadm. v\u00fd\u0161ku ze SRTM
@@ -136,6 +137,7 @@ function.saveconfig=Ulo\u017eit nastaven\u00ed
 function.diskcache=Ulo\u017eit mapy na disk
 function.managetilecache=Upravit cache map
 function.getweatherforecast=St\u00e1hnout p\u0159edpov\u011b\u010f po\u010das\u00ed
+function.setaltitudetolerance=Nastavit toleranci v\u00fd\u0161ky
 
 # Dialogs
 dialog.exit.confirm.title=Ukon\u010dit GpsPrune
@@ -358,7 +360,7 @@ dialog.gpsies.nonefound=Nenalezeny \u017e\u00e1dn\u00e9 stopy
 dialog.mapillary.nonefound=Nenalezeny \u017e\u00e1dn\u00e9 fotografie
 dialog.wikipedia.column.name=N\u00e1zev \u010dl\u00e1nku
 dialog.wikipedia.column.distance=Vzd\u00e1lenost
-dialog.wikipedia.nonefound=Nenalezeny \u017e\u00e1dn\u00e9 body
+dialog.wikipedia.nonefound=Nebyly nalezeny \u017e\u00e1dn\u00e9 z\u00e1znamy na wikipedii
 dialog.osmpois.column.name=N\u00e1zev
 dialog.osmpois.column.type=Typ
 dialog.osmpois.nonefound=Nenalezeny \u017e\u00e1dn\u00e9 body
@@ -395,6 +397,7 @@ dialog.correlate.timestamp.end=Konec
 dialog.correlate.audioselect.intro=Vyberte jednu z t\u011bchto slad\u011bn\u00fdch nahr\u00e1vek pro ur\u010den\u00ed \u010dasov\u00e9ho posunu
 dialog.correlate.select.audioname=N\u00e1zev audionahr\u00e1vky
 dialog.correlate.select.audiolater=Audio pozd\u011bj\u0161\u00ed
+dialog.rearrangewaypoints.desc=Zvolte c\u00edl a po\u0159ad\u00ed \u0159azen\u00ed bod\u016f
 dialog.rearrangephotos.desc=Vyberte um\u00edst\u011bn\u00ed a uspo\u0159\u00e1d\u00e1n\u00ed bod\u016f fotografi\u00ed
 dialog.rearrange.tostart=P\u0159en\u00e9st na za\u010d\u00e1tek
 dialog.rearrange.toend=P\u0159en\u00e9st na konec
@@ -504,6 +507,7 @@ dialog.diskcache.deleted=Smaz\u00e1no %d soubor\u016f z cache
 dialog.deletefieldvalues.intro=Vyberte pole, kter\u00e9 se m\u00e1 z aktu\u00e1ln\u00edho rozmez\u00ed odstranit
 dialog.deletefieldvalues.nofields=V tomto rozmez\u00ed nelze smazat \u017e\u00e1dn\u00e9 pole
 dialog.displaysettings.linewidth=Zvolte tlou\u0161\u0165ku \u010d\u00e1ry, kterou se nakresl\u00ed stopa (1-4)
+dialog.displaysettings.antialias=Pou\u017e\u00edt antialiasing
 dialog.downloadosm.desc=Potvr\u010fte, \u017ee se maj\u00ed k dan\u00e9 oblasti st\u00e1hnout data OSM:
 dialog.displaysettings.wpicon.plectrum=Trs\u00e1tko
 dialog.displaysettings.wpicon.ring=Krou\u017Eek
@@ -731,7 +735,7 @@ units.degreesfahrenheit.short=\u00b0F
 logic.and=a
 logic.or=nebo
 
-# External urls
+# External urls and services
 url.googlemaps=maps.google.cz
 wikipedia.lang=cs
 openweathermap.lang=en
@@ -810,8 +814,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 9090b8bbbad80530611e439187d1c58444b8bd1d..8592855449f1abaad154338494a74765ecc4fa60 100644 (file)
@@ -890,8 +890,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 147e847fc92a5ae5cd99ead8d925b49fe46b017d..a4b85ef275f25205ae832dd74c478ae31c23bf28 100644 (file)
@@ -885,8 +885,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 98e26f8780be7340fc0fb27620d9959b693dceb1..045eccdee6592213cf15feb5dafcc8e0759f16f5 100644 (file)
@@ -902,8 +902,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 02aad606cee7413419521926e3de9127d00f83aa..e84156f3362d36923aa5e788817eb716599de86c 100644 (file)
@@ -886,8 +886,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 ee1a126405196e57983347357da8cf191df74c50..1b99a719144bcc9c3bde3f718d0b2c1b12cbecd1 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
@@ -513,6 +514,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
@@ -701,6 +703,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
@@ -715,6 +718,7 @@ fieldname.duration=Kesto
 fieldname.speed=Nopeus
 fieldname.verticalspeed=Pystysuora nopeus
 fieldname.description=Kuvaus
+fieldname.comment=Kommentti
 fieldname.mediafilename=Median tiedostonimi
 
 # Measurement units
@@ -833,8 +837,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 38254fcc77b6013163449f6adb8ad3f7df2b0701..ba4666af2ac00b8905c7a14295622a121082627f 100644 (file)
@@ -37,6 +37,7 @@ menu.view.browser.openstreetmap=Openstreetmap
 menu.view.browser.mapquest=Mapquest
 menu.view.browser.yahoo=Cartes Yahoo
 menu.view.browser.bing=Cartes Bing
+menu.view.browser.inlinemap=Carte du roller
 menu.settings=Pr\u00e9f\u00e9rences
 menu.settings.onlinemode=Charger cartes depuis internet
 menu.settings.autosave=Sauver automatiquement en quittant
@@ -97,7 +98,9 @@ function.rearrangewaypoints=R\u00e9arranger les points de navigation
 function.convertnamestotimes=Convertir les noms de points de navigation en horodatages
 function.deletefieldvalues=Effacer les valeurs du champ
 function.findwaypoint=Trouver un waypoint
-function.pastecoordinates=Coller les coordonn\u00e9es
+function.pastecoordinates=Saisir coordonn\u00e9es d'un point
+function.pastecoordinatelist=Saisir liste de coordonn\u00e9es
+function.enterpluscode=Saisir code plus
 function.charts=Graphiques
 function.show3d=Montrer en 3D
 function.distances=Distances
@@ -122,6 +125,7 @@ function.duplicatepoint=Dupliquer le point
 function.setcolours=Choisir les couleurs
 function.setdisplaysettings=Pr\u00e9f\u00e9rences d'affichage
 function.setlanguage=Choisir la langue
+function.projectpoint=Projeter le point
 function.connecttopoint=Relier au point
 function.disconnectfrompoint=D\u00e9tacher du point
 function.removephoto=Retirer la photo
@@ -210,8 +214,10 @@ dialog.gpsbabel.filter.simplify.maxpoints=Nombre de points <
 dialog.gpsbabel.filter.simplify.maxerror=ou erreur <
 dialog.gpsbabel.filter.simplify.crosstrack=d\u00e9viation
 dialog.gpsbabel.filter.simplify.length=changement de longeur
+dialog.gpsbabel.filter.simplify.relative=par rapport \u00e0 la pr\u00e9cision
 dialog.gpsbabel.filter.distance.distance=Si la distance <
 dialog.gpsbabel.filter.distance.time=et difference de temps <
+dialog.gpsbabel.filter.interpolate.intro=Ajouter des points suppl\u00e9mentaires
 dialog.gpsbabel.filter.interpolate.distance=Si la distance >
 dialog.gpsbabel.filter.interpolate.time=ou difference de temps >
 dialog.saveoptions.title=Enregistrer le fichier
@@ -261,9 +267,10 @@ dialog.baseimage.zoom=Zoom
 dialog.baseimage.incomplete=Image incompl\u00e8te
 dialog.baseimage.tiles=Dalles
 dialog.baseimage.size=Taille de l'image
+dialog.exportimage.noimagepossible=Images de la carte doivent \u00eatre sauvegard\u00e9es dans un cache.
 dialog.exportimage.drawtrack=Dessiner la trace sur la carte
 dialog.exportimage.drawtrackpoints=Dessiner les points de trace
-dialog.exportimage.textscalepercent=Facteur d'echelle du texte (%)
+dialog.exportimage.textscalepercent=Facteur d'\u00e9chelle du texte (%)
 dialog.pointtype.desc=Sauvegarder ces types de points:
 dialog.pointtype.track=Points de la trace
 dialog.pointtype.waypoint=Waypoints
@@ -422,9 +429,13 @@ dialog.compress.summarylabel=Points \u00e0 supprimer
 dialog.compress.confirm=%d point(s) marqu\u00e9(s).\nSupprimer les points?
 dialog.compress.confirmnone=Pas de points marqu\u00e9s
 dialog.deletemarked.nonefound=Pas de donn\u00e9es \u00e0 effacer
-dialog.pastecoordinates.desc=Entrez ou collez les coordonn\u00e9es ici
+dialog.pastecoordinates.desc=Saisissez ou collez les coordonn\u00e9es ici
 dialog.pastecoordinates.coords=Coordonn\u00e9es
-dialog.pastecoordinates.nothingfound=V\u00e9rifier les coordonn\u00e9es et essayez \u00e0 nouveau
+dialog.pastecoordinates.nothingfound=V\u00e9rifiez les coordonn\u00e9es et essayez \u00e0 nouveau
+dialog.pastecoordinatelist.desc=Saisissez les coordonn\u00e9es des nouveaux points avec un point par ligne
+dialog.pluscode.desc=Entrez ou collez le code ici
+dialog.pluscode.code=Code Plus
+dialog.pluscode.nothingfound=V\u00e9rifiez le code et essayez \u00e0 nouveau
 dialog.help.help=Consultez la page\n https://gpsprune.activityworkshop.net/\npour plus de d\u00e9tails et des manuels utilisateur.
 dialog.about.version=Version
 dialog.about.build=Build
@@ -485,6 +496,7 @@ dialog.colourchooser.title=Choisissez la couleur
 dialog.colourchooser.red=Rouge
 dialog.colourchooser.green=Vert
 dialog.colourchooser.blue=Bleu
+dialog.colourer.intro=Un coloriste peut s\u00e9lectionner les couleurs pour les points de trace
 dialog.colourer.type=Crit\u00e8re de coloriste
 dialog.colourer.type.none=Aucun
 dialog.colourer.type.byfile=Selon fichier
@@ -523,9 +535,17 @@ dialog.deletefieldvalues.intro=Choisir le champ \u00e0 effacer pour l'\u00e9tend
 dialog.deletefieldvalues.nofields=L'\u00e9tendue actuelle n'a pas de champs \u00e0 effacer
 dialog.displaysettings.linewidth=L'\u00e9paisseur des lignes des traces (1-4)
 dialog.displaysettings.antialias=Anticr\u00e9nelage
+dialog.displaysettings.waypointicons=Ic\u00f4nes des waypoints
+dialog.displaysettings.wpicon.default=D\u00e9faut
+dialog.displaysettings.wpicon.ringpt=Disque
+dialog.displaysettings.wpicon.plectrum=Plectre
+dialog.displaysettings.wpicon.ring=Anneau
+dialog.displaysettings.wpicon.pin=Clouer
 dialog.displaysettings.size.small=Petit
 dialog.displaysettings.size.medium=Moyen
 dialog.displaysettings.size.large=Grand
+dialog.displaysettings.windowstyle=Style de fen\u00eatre (apr\u00e8s red\u00e9marrage)
+dialog.displaysettings.windowstyle.default=D\u00e9faut
 dialog.downloadosm.desc=Confirmer le t\u00e9l\u00e9chargement des donn\u00e9es OSM brutes pour la zone indiqu\u00e9e :
 dialog.searchwikipedianames.search=Chercher :
 dialog.weather.location=Location
@@ -551,16 +571,29 @@ dialog.weather.temp=Temp
 dialog.weather.humidity=Humidit\u00e9
 dialog.weather.creditnotice=Ces donn\u00e9es sont fournies par openweathermap.org. Consultez la page pour plus de d\u00e9tails.
 dialog.deletebydate.onlyonedate=Tous les points sont \u00e0 la m\u00eame date.
+dialog.deletebydate.intro=Pour chaque date, vous pouvez choisir de conserver ou de supprimer les points
 dialog.deletebydate.nodate=Sans horodatage
 dialog.deletebydate.column.keep=Garder
 dialog.deletebydate.column.delete=Supprimer
 dialog.setaltitudetolerance.text.metres=Limite (m\u00e8tres) pour les petites diff\u00e9rences d'altitude
 dialog.setaltitudetolerance.text.feet=Limite (pieds) pour les petites diff\u00e9rences d'altitude
+dialog.settimezone.intro=Ce fuseau horaire sera utilis\u00e9 pour afficher les horodatages des points
+dialog.settimezone.system=Utiliser fuseau du syst\u00e8me
+dialog.settimezone.custom=Utiliser le fuseau suivant:
+dialog.settimezone.list.toomany=Beaucoup trop de fuseaux
+dialog.settimezone.selectedzone=Fuseau horaire s\u00e9lectionn\u00e9
+dialog.settimezone.offsetfromutc=D\u00e9calage avec UTC
 dialog.autoplay.duration=Dur\u00e9e (sec)
 dialog.autoplay.usetimestamps=Utiliser information de temps
 dialog.autoplay.rewind=Retour au d\u00e9but
 dialog.autoplay.pause=Pause
 dialog.autoplay.play=Jouer
+dialog.markers.halves=Points \u00e0 mi-chemin
+dialog.markers.half.distance=Demi-distance
+dialog.markers.half.climb=Demi-mont\u00e9e
+dialog.markers.half.descent=Demi-descente
+dialog.projectpoint.desc=Saisissez la direction et la distance de la projection
+dialog.projectpoint.bearing=Azimut (degr\u00e8s du nord)
 
 # 3d window
 dialog.3d.title=Vue 3D de GpsPrune
@@ -579,6 +612,8 @@ confirm.addtimeoffset=D\u00e9calage ajout\u00e9
 confirm.addaltitudeoffset=D\u00e9calage d'altitude ajout\u00e9
 confirm.rearrangewaypoints=Waypoints r\u00e9arrang\u00e9s
 confirm.rearrangephotos=Photos r\u00e9arrang\u00e9es
+confirm.splitsegments=%d s\u00e9parations de segments ont \u00e9t\u00e9 effectu\u00e9es
+confirm.sewsegments=%d fusions de segments ont \u00e9t\u00e9 effectu\u00e9es
 confirm.cutandmove=S\u00e9lection d\u00e9plac\u00e9e
 confirm.pointsadded=%d points ajout\u00e9s
 confirm.convertnamestotimes=Noms de waypoints convertis
@@ -606,8 +641,9 @@ confirm.correlateaudios.multi=fichiers audio ont \u00e9t\u00e9 corr\u00e9l\u00e9
 
 # Tips
 tip.title=Astuce
-tip.useamapcache=By setting up a disk cache (Pr\u00e9f\u00e9rences -> Enregistrer les cartes sur le disque)\nyou can speed up the display and reduce network traffic.
-tip.learntimeparams=The results will be more accurate if you use\nTrace -> Apprentissage de l'estimation\non your recorded tracks.
+tip.useamapcache=Si vous configurez un cache (Pr\u00e9f\u00e9rences -> Enregistrer les cartes sur le disque)\nl'affichage sera plus rapide et les t\u00e9l\u00e9chargements seront r\u00e9duits.
+tip.learntimeparams=Les r\u00e9sultats seront plus pr\u00e9cis si GpsPrune peut\napprender la vitesse de vos traces\n(Trace -> Apprentissage de l'estimation).
+tip.usesrtmfor3d=Cette trace n'a pas d'altitudes.\nEn utilisant le SRTM, il est possible d'obtenir\ndes altitudes approximatives.
 tip.manuallycorrelateone=En corr\u00e9lant manuellement au moins une photo, le d\u00e9calage de temps peut \u00eatre calcul\u00e9 pour vous.
 
 # Buttons
@@ -721,6 +757,7 @@ fieldname.duration=Dur\u00e9e
 fieldname.speed=Vitesse
 fieldname.verticalspeed=Vitesse verticale
 fieldname.description=Description
+fieldname.comment=Commentaire
 fieldname.mediafilename=Nom de fichier
 
 # Measurement units
@@ -820,6 +857,7 @@ error.load.nopoints=Aucune coordonn\u00e9e trouv\u00e9e dans le fichier
 error.load.unknownxml=Format xml non-reconnu :
 error.load.noxmlinzip=Aucune xml fichier trouv\u00e9e dans le fichier
 error.load.othererror=Erreur \u00e0 la lecture du fichier :
+error.load.nopointsintext=Aucune coordonn\u00e9e trouv\u00e9e
 error.jpegload.dialogtitle=Erreur au chargement des photos
 error.jpegload.nofilesfound=Aucun fichier trouv\u00e9
 error.jpegload.nojpegsfound=Aucun fichier jpeg trouv\u00e9
@@ -840,11 +878,11 @@ 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
 error.cache.empty=Le dossier du cache est vide
 error.cache.cannotdelete=Effacement des dalles impossible
 error.tracksplit.nosplit=Impossible de s\u00e9parer les segments
+error.downloadsrtm.nocache=Les fichiers ne peuvent pas \u00eatre sauvegard\u00e9s.\nV\u00e9rifiez le cache.
+error.sewsegments.nothingdone=Aucune fusion n'a \u00e9t\u00e9 possible.\nIl y a %d segments dans la trace.
index 785d063b0c6492aa09a459c3d1615f4909ec20df..a35a433872c8fe6ba3d945ad47a2227813bcfad3 100644 (file)
@@ -887,8 +887,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 0d6d4114bb94673eaefd74d172f4b4267ac2d2a4..9fd82677734edebc3898db55f5ac6a8ed9dbcffd 100644 (file)
@@ -881,8 +881,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 619044224f21a1a370358db6e79761ef8e2d24ef..c7bc53f564b2fddb2950fdac331d39cfeb84b4f0 100644 (file)
@@ -884,8 +884,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 ec7a531991efc7477666e5e965fb0543f27f0acc..fc1ec4b738c07329b6a97c0240541e328d6e7a40 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
@@ -368,6 +370,8 @@ dialog.wikipedia.column.name=Tytu\u0142 artyku\u0142u
 dialog.wikipedia.column.distance=Odleg\u0142o\u015b\u0107
 dialog.wikipedia.nonefound=Brak wpis\u00f3w w wikipedii
 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?
@@ -524,11 +528,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
@@ -560,12 +569,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
@@ -830,6 +841,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
@@ -850,8 +862,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 55f60fa11d86d3ea1368d5d193d43765bfafb7cb..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
@@ -358,6 +359,8 @@ dialog.gpsies.description=Descri\u00e7\u00e3o
 dialog.gpsies.nodescription=Sem descri\u00e7\u00e3o
 dialog.gpsies.nonefound=Nenhuma rota encontrada
 dialog.mapillary.nonefound=Nenhuma foto encontrada
+dialog.osmpois.column.name=Nome
+dialog.osmpois.column.type=Tipo
 dialog.wikipedia.column.name=Nome do artigo
 dialog.wikipedia.column.distance=Dist\u00e2ncia
 dialog.wikipedia.nonefound=Nenhum artigo encontrado
@@ -418,6 +421,7 @@ dialog.deletemarked.nonefound=Nenhum dado dos pontos pode ser removido
 dialog.pastecoordinates.desc=Insira ou cole as coordenadas aqui
 dialog.pastecoordinates.coords=Coordenadas
 dialog.pastecoordinates.nothingfound=Por favor, verifique as coordenadas novamente
+dialog.pluscode.code=C\u00f3digo
 dialog.help.help=Por favor, veja\n https://gpsprune.activityworkshop.net/\npara mais informa\u00e7\u00f5es e guia do usu\u00e1rio.
 dialog.about.version=Vers\u00e3o
 dialog.about.build=Compila\u00e7\u00e3o
@@ -534,6 +538,7 @@ dialog.autoplay.usetimestamps=Usar data-hora
 dialog.autoplay.rewind=Rebobinar
 dialog.autoplay.pause=Suspender
 dialog.autoplay.play=Tocar
+dialog.projectpoint.bearing=Azimute (graus de N)
 
 # 3d window
 dialog.3d.title=Vista 3D do GpsPrune
@@ -682,6 +687,7 @@ map.overzoom=Nenhum mapa dispon\u00edvel neste n\u00edvel de amplia\u00e7\u00e3o
 # Field names
 fieldname.latitude=Latitude
 fieldname.longitude=Longitude
+fieldname.coordinates=Coordenadas
 fieldname.altitude=Altura
 fieldname.timestamp=Tempo
 fieldname.time=Tempo
@@ -696,6 +702,7 @@ fieldname.duration=Dura\u00e7\u00e3o
 fieldname.speed=Velocidade
 fieldname.verticalspeed=Velocidade vertical
 fieldname.description=Descri\u00e7\u00e3o
+fieldname.comment=Coment\u00e1rio
 fieldname.mediafilename=Arquivo
 
 # Measurement units
@@ -815,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 0d1876bfa6b8b1296da657542cea3ac4f2a2dfda..fe58ce6023cef92dfd69b321bec5b421fffe2d54 100644 (file)
@@ -850,8 +850,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 621c918863268a89ecc0bded3cdfdb55e57a2a24..de8eb44c22f391889af9c7bc08a897bbf3eaa58f 100644 (file)
@@ -882,8 +882,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 543a64fb9ff82f083bfdcece3fdbb125ecc29af1..5ffebf1d077c408045482f72b8f8d71c67cda9e9 100644 (file)
@@ -20,7 +20,7 @@ menu.range.end=S\u00e4tt till slutet av intervall
 menu.range.average=Skapa medelv\u00e4rdespunkt
 menu.range.reverse=V\u00e4nd intervall
 menu.range.mergetracksegments=Sl\u00e5 ihop sp\u00e5rsegment
-menu.range.cutandmove=Klipp och flytta urval
+menu.range.cutandmove=Klipp och flytta intervall
 menu.point=Punkt
 menu.point.editpoint=Redigera punkt
 menu.point.deletepoint=Radera punkt
@@ -113,7 +113,7 @@ function.setmapbg=V\u00e4lj bakgrundskarta
 function.setpaths=V\u00e4lj s\u00f6kv\u00e4gar f\u00f6r program
 function.selectsegment=Markera aktuellt segment
 function.splitsegments=Dela upp sp\u00e5ret i segment
-function.sewsegments=Sy ihop sp\u00e5r-segment
+function.sewsegments=Sy ihop sp\u00e5rsegment
 function.createmarkerwaypoints=Skapa markerings-waypoints
 function.lookupsrtm=H\u00e4mta h\u00f6jddata fr\u00e5n SRTM
 function.getwikipedia=H\u00e4mta n\u00e4rliggande Wikipedia-artiklar
@@ -167,11 +167,15 @@ 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
 dialog.delimiter.semicolon=Semikolon ;
 dialog.delimiter.other=Annat
+dialog.openoptions.deliminfo.records=rader, med
+dialog.openoptions.deliminfo.norecords=Inga rader
+dialog.openoptions.deliminfo.fields=f\u00e4lt
 dialog.openoptions.altitudeunits=H\u00f6jdenhet
 dialog.openoptions.speedunits=Hastighetsenheter
 dialog.openoptions.vertspeedunits=Enheter f\u00f6r vertikal hastighet
@@ -423,6 +427,7 @@ dialog.pastecoordinates.nothingfound=V\u00e4nligen kontrollera koordinaterna och
 dialog.pastecoordinatelist.desc=Fyll i koordinaterna f\u00f6r dom nya punkterna, en punkt per rad
 dialog.pluscode.desc=Fyll i pluss-koden (Open Location Code) h\u00e4r
 dialog.pluscode.code=Pluss-kod
+dialog.pluscode.nothingfound=Kontrollera koden och f\u00f6rs\u00f6k igen.
 dialog.help.help=L\u00e4s\n https://gpsprune.activityworkshop.net/\nf\u00f6r mer information och tips,\ninklusive en PDF-anv\u00e4ndarhandbok som man kan k\u00f6pa.
 dialog.about.version=Version
 dialog.about.build=Build
@@ -523,6 +528,7 @@ dialog.deletefieldvalues.nofields=Det finns inga f\u00e4lt att ta bort f\u00f6r
 dialog.displaysettings.linewidth=Linjetjocklek f\u00f6r sp\u00e5ren (1-4)
 dialog.displaysettings.antialias=Anv\u00e4nd kantutj\u00e4mning ("antialiasing")
 dialog.displaysettings.waypointicons=Waypoint-ikon
+dialog.displaysettings.wpicon.default=Standardmark\u00f6r
 dialog.displaysettings.wpicon.ringpt=Rund mark\u00f6r
 dialog.displaysettings.wpicon.plectrum=Plektrum
 dialog.displaysettings.wpicon.ring=Ring
@@ -531,6 +537,7 @@ dialog.displaysettings.size.small=Liten
 dialog.displaysettings.size.medium=Mellan
 dialog.displaysettings.size.large=Stor
 dialog.displaysettings.windowstyle=F\u00f6nster-stil (kr\u00e4ver omstart)
+dialog.displaysettings.windowstyle.default=Standard
 dialog.displaysettings.windowstyle.nimbus=Nimbus
 dialog.displaysettings.windowstyle.gtk=GTK
 dialog.downloadosm.desc=Bekr\u00e4fta att OSM-r\u00e5data laddas ner f\u00f6r det specifika omr\u00e5det:
@@ -557,22 +564,86 @@ dialog.weather.wind=Vind
 dialog.weather.temp=Temp
 dialog.weather.humidity=Fuktighet
 dialog.weather.creditnotice=Denna data har tillhandah\u00e5llits av openweathermap.org. Mer detaljer finns p\u00e5 deras hemsida.
+dialog.deletebydate.onlyonedate=Alla punkter \u00e4r fr\u00e5n samma datum.
+dialog.deletebydate.intro=F\u00f6r varje datum i sp\u00e5ret kan du v\u00e4lja att ta bort eller beh\u00e5lla punkterna
 dialog.deletebydate.nodate=Ingen tidsst\u00e4mpel
 dialog.deletebydate.column.keep=Beh\u00e5ll
 dialog.deletebydate.column.delete=Ta bort
+dialog.settimezone.intro=H\u00e4r kan du v\u00e4lja i vilken tidszon du vill visa punkternas tidsst\u00e4mplar
 dialog.settimezone.system=Anv\u00e4nd systemets tidszon
 dialog.settimezone.custom=Anv\u00e4nd f\u00f6ljande tidszon:
+dialog.settimezone.list.toomany=F\u00f6r m\u00e5nga f\u00f6r att v\u00e4lja
 dialog.settimezone.selectedzone=Vald tidszon
 dialog.settimezone.offsetfromutc=Offset fr\u00e5n UTC
+dialog.autoplay.duration=L\u00e4ngd (sekunder)
 dialog.autoplay.usetimestamps=Anv\u00e4nd tidsst\u00e4mplar fr\u00e5n punkter
 dialog.autoplay.rewind=Tillbaka till b\u00f6rjan
 dialog.autoplay.pause=Paus
 dialog.autoplay.play=Spela upp
+dialog.markers.halves=Halvv\u00e4gs-punkter
+dialog.markers.half.distance=Halva avst\u00e5ndet
+dialog.markers.half.climb=Halva kl\u00e4ttringen
+dialog.markers.half.descent=Halva nedstigningen
+dialog.projectpoint.desc=Fyll i riktning och avst\u00e5nd f\u00f6r att projicera denna punkt
+dialog.projectpoint.bearing=B\u00e4ring (grader fr\u00e5n N)
+
+# 3d window
+dialog.3d.title=GpsPrune 3D-vy
+dialog.3d.altitudefactor=F\u00f6rst\u00e4rkningsfaktor f\u00f6r h\u00f6jddata
+
+# Confirm messages
+confirm.loadfile=Data inladdat fr\u00e5n filen
+confirm.save.ok1=Det gick bra att spara
+confirm.save.ok2=punkter till filen
+confirm.deletepoint.single=datapunkt togs bort
+confirm.deletepoint.multi=datapunkter togs bort
+confirm.point.edit=punkt redigerad
+confirm.mergetracksegments=Sp\u00e5r-segment ihopslagna
+confirm.reverserange=Intervall v\u00e4ndes
+confirm.addtimeoffset=Tids-offset tillagt
+confirm.addaltitudeoffset=H\u00f6jdoffset tillagt
+confirm.rearrangewaypoints=Waypoint:ar omarrangerade
+confirm.rearrangephotos=Foton omarrangerade
+confirm.splitsegments=segment delades upp i %d delar
+confirm.sewsegments=segment slogs ihop p\u00e5 %d st\u00e4llen
+confirm.cutandmove=Intervall flyttat
+confirm.pointsadded=%d punkter adderade
+confirm.convertnamestotimes=Waypoint-namn konverterade
+confirm.saveexif.ok=%d foton sparade
+confirm.undo.single=operation \u00e5terst\u00e4lld
+confirm.undo.multi=operationer \u00e5terst\u00e4llda
+confirm.jpegload.single=foto lades till
+confirm.jpegload.multi=foton lades till
+confirm.media.connect=media kopplats
+confirm.photo.disconnect=foto kopplats bort
+confirm.audio.disconnect=ljud kopplats bort
+confirm.media.removed=togs bort
+confirm.correlatephotos.single=foto korrelerat
+confirm.correlatephotos.multi=foton korrelerade
+confirm.createpoint=punkt skapad
+confirm.rotatephoto=foto roterat
+confirm.running=P\u00e5g\u00e5r...
+confirm.lookupsrtm=Hittade %d h\u00f6jd-datav\u00e4rden
+confirm.downloadsrtm=Laddade ner %d filer till cache:n
+confirm.downloadsrtm.1=Laddade ner %d fil till cache:n
+confirm.deletefieldvalues=F\u00e4lt-v\u00e4rden borttagna
+confirm.audioload=Ljud-filer adderade
+confirm.correlateaudios.single=ljud korrelerat
+confirm.correlateaudios.multi=ljud korrelerade
+
+# Tips, shown just once when appropriate
+tip.title=Tips
+tip.useamapcache=Genom att definiera en cache-mapp (Inst\u00e4llningar -> Spara kartor p\u00e5 h\u00e5rddisken)\nkan du snabba upp visningen och reducera datatrafiken.
+tip.learntimeparams=Resultaten f\u00e5r b\u00e4ttre nogrannhet om man f\u00f6rst anv\u00e4nder\nSp\u00e5r -> L\u00e4r upp tidsuppskattningsparametrar\np\u00e5 dina sp\u00e5r.
+tip.downloadsrtm=Du kan snabba upp detta genom att definiera en cachemapp\nf\u00f6r att spara SRTM-data lokalt.
+tip.usesrtmfor3d=Detta sp\u00e5r saknar h\u00f6jddata.\nDu kan anv\u00e4nda SRTM-funktioner f\u00f6r att f\u00e5 uppskattad\nh\u00f6jddata f\u00f6r 3d-vyn.
+tip.manuallycorrelateone=Genom att manuellt koppla \u00e5tminstone ett objekt kan tids-offset ber\u00e4knas f\u00f6r dig.
 
 # Buttons
 button.ok=OK
 button.back=Bak\u00e5t
 button.next=N\u00e4sta
+button.finish=Avsluta
 button.cancel=Avbryt
 button.overwrite=Skriv \u00f6ver
 button.moveup=Flytta upp
@@ -580,6 +651,7 @@ button.movedown=Flytta ner
 button.edit=Redigera
 button.exit=Avsluta
 button.close=St\u00e4ng
+button.continue=Forts\u00e4tta
 button.yes=Ja
 button.no=Nej
 button.yestoall=Ja till alla
@@ -592,6 +664,7 @@ button.preview=F\u00f6rhandsvisa
 button.load=Ladda in
 button.guessfields=Gissa f\u00e4lt
 button.showwebpage=Visa hemsida
+button.check=Kontrollera
 button.resettodefaults=\u00c5terst\u00e4ll till default
 button.browse=Bl\u00e4ddra...
 button.addnew=L\u00e4gg till ny
@@ -599,6 +672,18 @@ button.delete=Ta bort
 button.manage=Hantera
 button.combine=Kombinera
 
+# File types
+filetype.txt=TXT-filer
+filetype.jpeg=JPG-filer
+filetype.kmlkmz=KML- & KMZ-filer
+filetype.kml=KML-filer
+filetype.kmz=KMZ-filer
+filetype.gpx=GPX-filer
+filetype.pov=POV-filer
+filetype.svg=SVG-filer
+filetype.png=PNG-filer
+filetype.audio=MP3-, OGG- & WAV-filer
+
 # Display components
 display.nodata=Ingen data laddad
 display.noaltitudes=Sp\u00e5rdatan saknar h\u00f6jddata
@@ -630,6 +715,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
@@ -657,8 +743,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
@@ -704,4 +792,71 @@ logic.and=och
 logic.or=eller
 
 # External urls and services
+url.googlemaps=maps.google.se
+wikipedia.lang=sv
 openweathermap.lang=se
+webservice.peakfinder=\u00d6ppna Peakfinder.org
+webservice.geohack=\u00d6ppna Geohack-sidan
+
+# Cardinals for 3d plots
+cardinal.n=N
+cardinal.s=S
+cardinal.e=\u00d6
+cardinal.w=V
+
+# Undo operations
+undo.load=Ladda data
+undo.loadphotos=Ladda foton
+undo.loadaudios=Ladda ljudklipp
+undo.editpoint=redigera punkt
+undo.deletepoint=ta bort punkt
+undo.removephoto=ta bort foto
+undo.removeaudio=ta bort ljudklipp
+undo.deleterange=ta bort intervall
+undo.croptrack=besk\u00e4r sp\u00e5r
+undo.deletemarked=ta bort punkter
+undo.insert=infoga punkter
+undo.reverse=v\u00e4nd intervall
+undo.mergetracksegments=sl\u00e5 ihop sp\u00e5rsegment
+undo.splitsegments=dela upp sp\u00e5ret i segment
+undo.sewsegments=sy ihop sp\u00e5rsegment
+undo.addtimeoffset=l\u00e4gg till tids-offset
+undo.addaltitudeoffset=l\u00e4gg till h\u00f6jd-offset
+undo.rearrangewaypoints=omarrangera waypoints
+undo.cutandmove=flytta intervall
+undo.connect=koppla
+undo.disconnect=koppla bort
+undo.correlatephotos=korrelera foton
+undo.rearrangephotos=omarrangera foton
+undo.createpoint=skapa punkt
+undo.rotatephoto=rotera foto
+undo.convertnamestotimes=konvertera namn till tidpunkter
+undo.lookupsrtm=kolla upp h\u00f6jddata via SRTM
+undo.deletefieldvalues=ta bort f\u00e4lt-v\u00e4rden
+undo.correlateaudios=korrelera ljudklipp
+
+# Error messages
+error.save.dialogtitle=Fel vid sparande av data
+error.save.nodata=Ingen data att spara
+error.save.failed=Misslyckades att spara datan till fil
+error.saveexif.filenotfound=Misslyckades att hitta foto-fil
+error.saveexif.cannotoverwrite1=Foto-fil
+error.saveexif.cannotoverwrite2=\u00e4r skrivskyddad och kan inte skrivas \u00f6ver. Spara som kopia?
+error.saveexif.failed=Misslyckades att spara %d foton.
+error.saveexif.forced=%d foton kr\u00e4vde forcering
+error.load.dialogtitle=Fel vid l\u00e4sning av data
+error.load.noread=Kan inte l\u00e4sa fil
+error.load.nopoints=Ingen koordinat-information hittades i filen
+error.load.unknownxml=K\u00e4nde inte igen xml-formatet:
+error.load.noxmlinzip=Ingen xml-fil hittades inuti zip-filen
+error.load.othererror=Fel vid l\u00e4sning av fil:
+error.load.nopointsintext=Ingen koordinat-information kunde hittas
+error.jpegload.dialogtitle=Fel vid l\u00e4sning av foton
+error.jpegload.nofilesfound=Ingen fil hittades
+error.jpegload.nojpegsfound=Ingen jpeg-fil hittades
+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 405a5366e37528360b3cacc7fe1c0f71d6309d28..2c30761268d941bad44eb0c810542ca40d7f6d5b 100644 (file)
@@ -1,11 +1,11 @@
-GpsPrune version 20.1
+GpsPrune version 20.4
 =====================
 
 GpsPrune is an application for viewing, editing and managing coordinate data from GPS systems,
 including format conversion, charting, 3d visualisation, audio and photo correlation, and online resource lookup.
 Full details can be found at https://gpsprune.activityworkshop.net/
 
-GpsPrune is copyright 2006-2020 activityworkshop.net and distributed under the terms of the Gnu GPL version 2.
+GpsPrune is copyright 2006-2021 activityworkshop.net and distributed under the terms of the Gnu GPL version 2.
 You may freely use the software, and may help others to freely use it too.  For further information
 on your rights and how they are protected, see the included license.txt file.
 
@@ -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.1.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,31 @@ 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.1.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:
+  - If a waypoint is selected, the distances function should use it
+  - If a point list has been pasted, closing should prompt about unsaved data
+  - Some additional translations
+  - Switching SRTM downloads to use kurviger.de instead of usgs.gov (thanks, kurviger.de!)
+
+New with version 20.2
+=====================
+The following fixes and additions were made since version 20.1:
+  - Fix for intermittent startup problems
+  - Addition of Swedish (erikiiofph7)
+  - Allow four digits for adding day offsets (fperrin)
+
 New with version 20.1
 =====================
 The following fixes and additions were made since version 20:
index 8bfd0905f857b5c744ca8a25c9455da656dbd516..f7de8f73485ee87fba0789730a4c0e20b85243eb 100644 (file)
@@ -347,6 +347,7 @@ public class GpxExporter extends GenericFunction implements Runnable
                                 + " " + _exportFile.getAbsolutePath());
                        // export successful so need to close dialog and return
                        _dialog.dispose();
+                       _app.informDataSaved();
                        return;
                }
                catch (IOException ioe)
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);
+       }
+}