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.
# Build script
set -e
# Version number
-PRUNENAME=gpsprune_20
+PRUNENAME=gpsprune_20.4
# remove compile directory
rm -rf compile
# remove dist directory
<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>
-version=20
+version=20.4
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);
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);
}
/**
* 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
*/
/** 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;
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;
}
// timestamps
+ if (inPoint.getSegmentStart())
+ {
+ // reset movingTimestamp for moving time at the start
+ // of each segment
+ _movingTimestamp = null;
+ }
if (inPoint.hasTimestamp())
{
Timestamp currTstamp = inPoint.getTimestamp();
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;
}
_movingMilliseconds += millisLater;
}
}
+ _movingTimestamp = currTstamp;
}
else {
_timesIncomplete = true;
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());
}
_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);
}
}
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;
+ }
}
// 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++;
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"};
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)
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;
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
/** 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
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
_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;
package tim.prune.gui.map;
-import java.util.regex.Matcher;
import tim.prune.I18nManager;
/**
_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)
{
{
// 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)
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
*/
--- /dev/null
+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;
+ }
+}
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);
- }
}
}
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
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
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
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
# Text entries for the GpsPrune application
-# Czech entries thanks to prot_d
+# Czech entries thanks to prot_d, jirislaby
# Menu entries
menu.file=Soubor
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
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
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
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
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
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
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
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
logic.and=a
logic.or=nebo
-# External urls
+# External urls and services
url.googlemaps=maps.google.cz
wikipedia.lang=cs
openweathermap.lang=en
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.
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
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
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
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
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
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
# Field names
fieldname.latitude=Leveysaste (Lat.)
fieldname.longitude=Pituusaste (Long.)
+fieldname.coordinates=Koordinaatit
fieldname.altitude=Korkeus
fieldname.timestamp=Aikaleima
fieldname.time=Aika
fieldname.speed=Nopeus
fieldname.verticalspeed=Pystysuora nopeus
fieldname.description=Kuvaus
+fieldname.comment=Kommentti
fieldname.mediafilename=Median tiedostonimi
# Measurement units
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
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
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
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
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
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
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
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
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
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
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
# 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
fieldname.speed=Vitesse
fieldname.verticalspeed=Vitesse verticale
fieldname.description=Description
+fieldname.comment=Commentaire
fieldname.mediafilename=Nom de fichier
# Measurement units
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
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.
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
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
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
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
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
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
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
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?
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
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
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
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
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
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
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
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
# Field names
fieldname.latitude=Latitude
fieldname.longitude=Longitude
+fieldname.coordinates=Coordenadas
fieldname.altitude=Altura
fieldname.timestamp=Tempo
fieldname.time=Tempo
fieldname.speed=Velocidade
fieldname.verticalspeed=Velocidade vertical
fieldname.description=Descri\u00e7\u00e3o
+fieldname.comment=Coment\u00e1rio
fieldname.mediafilename=Arquivo
# Measurement units
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
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
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
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
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
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
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
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
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:
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
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
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
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
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
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
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
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
"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,
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;
_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
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
-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.
=======
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
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:
+ " " + _exportFile.getAbsolutePath());
// export successful so need to close dialog and return
_dialog.dispose();
+ _app.informDataSaved();
return;
}
catch (IOException ioe)
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;
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;
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);
// 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);
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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());
+ }
+}
--- /dev/null
+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."));
+ }
+}
--- /dev/null
+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");
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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));
+ }
+ }
+}
--- /dev/null
+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);
+ }
+}