From: activityworkshop Date: Mon, 10 May 2021 17:41:33 +0000 (+0200) Subject: Version 20.4, May 2021 X-Git-Tag: v20.4.fp1~22 X-Git-Url: http://gitweb.fperrin.net/?p=GpsPrune.git;a=commitdiff_plain;h=HEAD Version 20.4, May 2021 --- diff --git a/README.md b/README.md index 1a1aa06..169f8ee 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,5 @@ GpsPrune is a map-based application for viewing, editing and converting coordina It's a cross-platform java application, and its home page is at https://gpsprune.activityworkshop.net . -Here on github you'll find all the sources from version 1 to the current version 20, and in the wiki at https://github.com/activityworkshop/GpsPrune/wiki there's the beginning of a translation effort for anyone to contribute. -Currently just the Spanish translations and some missing French texts are online, to see whether it's a workable idea or not. Please help with these if you can. +Here on github you'll find all the sources from version 1 to the current version 20.4, and in the wiki at https://github.com/activityworkshop/GpsPrune/wiki there's the beginning of a translation effort for anyone to contribute. +Currently just the missing French texts and Polish texts are online, to see whether it's a workable idea or not. Please help with these if you can. diff --git a/buildtools/build.sh b/buildtools/build.sh index 193244b..f4f00f9 100644 --- a/buildtools/build.sh +++ b/buildtools/build.sh @@ -1,7 +1,7 @@ # Build script set -e # Version number -PRUNENAME=gpsprune_20 +PRUNENAME=gpsprune_20.4 # remove compile directory rm -rf compile # remove dist directory diff --git a/buildtools/pom.xml b/buildtools/pom.xml index 15b6562..b079793 100644 --- a/buildtools/pom.xml +++ b/buildtools/pom.xml @@ -7,7 +7,7 @@ tim.prune gpsprune - 20 + 20.4 jar tim.prune.gpsprune @@ -44,12 +44,25 @@ j3dutils ${j3dutils.version} + + org.junit.jupiter + junit-jupiter-engine + 5.7.1 + test + + + org.junit.jupiter + junit-jupiter-api + 5.7.1 + test + ${project.build.directory}/classes ${project.artifactId}_${project.version} - ${project.basedir}/ + ${project.basedir}/src + ${project.basedir}/test ${project.basedir}/src/ @@ -78,6 +91,11 @@ maven-compiler-plugin 3.8.0 + + + -Xlint:deprecation + + maven-jar-plugin @@ -126,7 +144,14 @@ ${app.mainClass} - + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + diff --git a/buildtools/version.properties b/buildtools/version.properties index fe5699b..15c4a26 100644 --- a/buildtools/version.properties +++ b/buildtools/version.properties @@ -1 +1 @@ -version=20 +version=20.4 diff --git a/src/tim/prune/GpsPrune.java b/src/tim/prune/GpsPrune.java index 8d80875..6644642 100644 --- a/src/tim/prune/GpsPrune.java +++ b/src/tim/prune/GpsPrune.java @@ -38,9 +38,9 @@ import tim.prune.gui.profile.ProfileChart; public class GpsPrune { /** Version number of application, used in about screen and for version check */ - public static final String VERSION_NUMBER = "20.3"; + public static final String VERSION_NUMBER = "20.4"; /** Build number, just used for about screen */ - public static final String BUILD_NUMBER = "385"; + public static final String BUILD_NUMBER = "387"; /** Static reference to App object */ private static App APP = null; diff --git a/src/tim/prune/data/RangeStats.java b/src/tim/prune/data/RangeStats.java index d8461c4..b32f12f 100644 --- a/src/tim/prune/data/RangeStats.java +++ b/src/tim/prune/data/RangeStats.java @@ -11,7 +11,7 @@ public class RangeStats private boolean _foundTrackPoint = false; protected AltitudeRange _totalAltitudeRange = new AltitudeRange(); protected AltitudeRange _movingAltitudeRange = new AltitudeRange(); - private Timestamp _earliestTimestamp = null, _latestTimestamp = null; + private Timestamp _earliestTimestamp = null, _latestTimestamp = null, _movingTimestamp = null; private long _movingMilliseconds = 0L; private boolean _timesIncomplete = false; private boolean _timesOutOfSequence = false; @@ -75,6 +75,12 @@ public class RangeStats } // timestamps + if (inPoint.getSegmentStart()) + { + // reset movingTimestamp for moving time at the start + // of each segment + _movingTimestamp = null; + } if (inPoint.hasTimestamp()) { Timestamp currTstamp = inPoint.getTimestamp(); @@ -84,10 +90,11 @@ public class RangeStats if (_latestTimestamp == null || currTstamp.isAfter(_latestTimestamp)) { _latestTimestamp = currTstamp; } + // Work out duration without segment gaps - if (!inPoint.getSegmentStart() && _prevPoint != null && _prevPoint.hasTimestamp()) + if (_movingTimestamp != null) { - long millisLater = currTstamp.getMillisecondsSince(_prevPoint.getTimestamp()); + long millisLater = currTstamp.getMillisecondsSince(_movingTimestamp); if (millisLater < 0) { _timesOutOfSequence = true; } @@ -95,6 +102,7 @@ public class RangeStats _movingMilliseconds += millisLater; } } + _movingTimestamp = currTstamp; } else { _timesIncomplete = true; diff --git a/src/tim/prune/function/srtm/LookupSrtmFunction.java b/src/tim/prune/function/srtm/LookupSrtmFunction.java index c5986db..cccc7ed 100644 --- a/src/tim/prune/function/srtm/LookupSrtmFunction.java +++ b/src/tim/prune/function/srtm/LookupSrtmFunction.java @@ -267,7 +267,7 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable // try to download to cache TileDownloader cacher = new TileDownloader(); TileDownloader.Result result = cacher.downloadTile(inUrl); - System.out.println("Result: " + result); + // System.out.println("Result: " + result); if (result == TileDownloader.Result.DOWNLOADED) { _numCached++; diff --git a/src/tim/prune/gui/map/MapCanvas.java b/src/tim/prune/gui/map/MapCanvas.java index a469e99..1791892 100644 --- a/src/tim/prune/gui/map/MapCanvas.java +++ b/src/tim/prune/gui/map/MapCanvas.java @@ -691,8 +691,9 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe final int[] xPixels = new int[numPoints]; final int[] yPixels = new int[numPoints]; - final int pointSeparationForArrowsSqd = 350; + final int pointSeparationForArrowsSqd = 400; final int pointSeparation1dForArrows = (int) (Math.sqrt(pointSeparationForArrowsSqd) * 0.7); + final int hugePointSeparationForArrows = 120; // try to set line width for painting if (inG instanceof Graphics2D) @@ -765,35 +766,38 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe pointsPainted = true; // Now consider whether we need to draw an arrow as well - if (drawArrows - && !drawnLastArrow - && (Math.abs(prevX-px) > pointSeparation1dForArrows || Math.abs(prevY-py) > pointSeparation1dForArrows)) + if (drawArrows) { - final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py); - if (pointSeparationSqd > pointSeparationForArrowsSqd) + final double pointDist = Math.max(Math.abs(prevX - px), Math.abs(prevY - py)); + final int separationLimit = (drawnLastArrow ? hugePointSeparationForArrows : pointSeparation1dForArrows); + if (pointDist > separationLimit) { - final double midX = (prevX + px) / 2.0; - final double midY = (prevY + py) / 2.0; - final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight; - if (midPointVisible) + final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py); + if (pointSeparationSqd > pointSeparationForArrowsSqd) { - final double alpha = Math.atan2(py - prevY, px - prevX); - //System.out.println("Draw arrow from (" + prevX + "," + prevY + ") to (" + px + "," + py - // + ") with angle" + (int) (alpha * 180/Math.PI)); - final double MID_TO_VERTEX = 3.0; - final double arrowX = MID_TO_VERTEX * Math.cos(alpha); - final double arrowY = MID_TO_VERTEX * Math.sin(alpha); - final double vertexX = midX + arrowX; - final double vertexY = midY + arrowY; - inG.drawLine((int)(midX-arrowX-2*arrowY), (int)(midY-arrowY+2*arrowX), (int)vertexX, (int)vertexY); - inG.drawLine((int)(midX-arrowX+2*arrowY), (int)(midY-arrowY-2*arrowX), (int)vertexX, (int)vertexY); + final double midX = (prevX + px) / 2.0; + final double midY = (prevY + py) / 2.0; + final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight; + if (midPointVisible) + { + final double alpha = Math.atan2(py - prevY, px - prevX); + //System.out.println("Draw arrow from (" + prevX + "," + prevY + ") to (" + px + "," + py + // + ") with angle" + (int) (alpha * 180/Math.PI)); + final double MID_TO_VERTEX = 3.0; + final double arrowX = MID_TO_VERTEX * Math.cos(alpha); + final double arrowY = MID_TO_VERTEX * Math.sin(alpha); + final double vertexX = midX + arrowX; + final double vertexY = midY + arrowY; + inG.drawLine((int)(midX-arrowX-2*arrowY), (int)(midY-arrowY+2*arrowX), (int)vertexX, (int)vertexY); + inG.drawLine((int)(midX-arrowX+2*arrowY), (int)(midY-arrowY-2*arrowX), (int)vertexX, (int)vertexY); + } + drawnLastArrow = midPointVisible; } - drawnLastArrow = midPointVisible; } - } - else - { - drawnLastArrow = false; + else + { + drawnLastArrow = false; + } } } prevX = px; prevY = py; diff --git a/src/tim/prune/gui/map/MapSource.java b/src/tim/prune/gui/map/MapSource.java index c4e2946..aa7127d 100644 --- a/src/tim/prune/gui/map/MapSource.java +++ b/src/tim/prune/gui/map/MapSource.java @@ -2,8 +2,7 @@ package tim.prune.gui.map; import java.net.MalformedURLException; import java.net.URL; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + /** * Class to represent any map source, whether an OsmMapSource @@ -16,9 +15,6 @@ public abstract class MapSource /** File extensions */ protected String[] _extensions = null; - /** Regular expression for catching server wildcards */ - protected static final Pattern WILD_PATTERN = Pattern.compile("^(.*)\\[(.*)\\](.*)$"); - /** * @return the number of layers used in this source @@ -120,32 +116,6 @@ public abstract class MapSource return urlstr; } - /** - * Fix the site name by stripping off protocol and www. - * This is used to create the file path for disk caching - * @param inUrl url to strip - * @return stripped url - */ - protected static String fixSiteName(String inUrl) - { - if (inUrl == null || inUrl.equals("")) {return null;} - String url = inUrl.toLowerCase(); - int idx = url.indexOf("://"); - if (idx >= 0) {url = url.substring(idx + 3);} - if (url.startsWith("www.")) {url = url.substring(4);} - // Strip out any "[.*]" as well - if (url.indexOf('[') >= 0) - { - Matcher matcher = WILD_PATTERN.matcher(url); - if (matcher.matches()) { - url = matcher.group(1) + matcher.group(3); - if (url.length() > 1 && url.charAt(0) == '.') { - url = url.substring(1); - } - } - } - return url; - } /** * @return string which can be written to the Config diff --git a/src/tim/prune/gui/map/MffMapSource.java b/src/tim/prune/gui/map/MffMapSource.java index 42806fc..fe3e5a6 100644 --- a/src/tim/prune/gui/map/MffMapSource.java +++ b/src/tim/prune/gui/map/MffMapSource.java @@ -32,8 +32,8 @@ public class MffMapSource extends MapSource _baseUrls[0] = fixBaseUrl(inUrl1); _baseUrls[1] = fixBaseUrl(inUrl2); _siteNames = new String[2]; - _siteNames[0] = fixSiteName(_baseUrls[0]); - _siteNames[1] = fixSiteName(_baseUrls[1]); + _siteNames[0] = SiteNameUtils.convertUrlToDirectory(_baseUrls[0]); + _siteNames[1] = SiteNameUtils.convertUrlToDirectory(_baseUrls[1]); _extensions = new String[2]; _extensions[0] = inExt1; _extensions[1] = inExt2; diff --git a/src/tim/prune/gui/map/OsmMapSource.java b/src/tim/prune/gui/map/OsmMapSource.java index 925fcf6..0cb579d 100644 --- a/src/tim/prune/gui/map/OsmMapSource.java +++ b/src/tim/prune/gui/map/OsmMapSource.java @@ -1,6 +1,5 @@ package tim.prune.gui.map; -import java.util.regex.Matcher; import tim.prune.I18nManager; /** @@ -84,8 +83,8 @@ public class OsmMapSource extends MapSource _extensions[0] = inExt1; _extensions[1] = inExt2; _siteNames = new String[2]; - _siteNames[0] = fixSiteName(_baseUrls[0]); - _siteNames[1] = fixSiteName(_baseUrls[1]); + _siteNames[0] = SiteNameUtils.convertUrlToDirectory(_baseUrls[0]); + _siteNames[1] = SiteNameUtils.convertUrlToDirectory(_baseUrls[1]); // Swap layers if second layer given without first if (_baseUrls[0] == null && _baseUrls[1] != null) { @@ -157,7 +156,7 @@ public class OsmMapSource extends MapSource { // Check if the base url has a [1234], if so replace at random StringBuffer url = new StringBuffer(); - url.append(pickServerUrl(_baseUrls[inLayerNum])); + url.append(SiteNameUtils.pickServerUrl(_baseUrls[inLayerNum])); url.append(inZoom).append('/').append(inX).append('/').append(inY); url.append('.').append(getFileExtension(inLayerNum)); if (_apiKey != null) @@ -175,36 +174,6 @@ public class OsmMapSource extends MapSource return _maxZoom; } - /** - * If the base url contains something like [1234], then pick a server - * @param inBaseUrl base url - * @return modified base url - */ - protected static final String pickServerUrl(String inBaseUrl) - { - if (inBaseUrl == null || inBaseUrl.indexOf('[') < 0) { - return inBaseUrl; - } - // Check for [.*] (once only) - // Only need to support one, make things a bit easier - final Matcher matcher = WILD_PATTERN.matcher(inBaseUrl); - // if not, return base url unchanged - if (!matcher.matches()) { - return inBaseUrl; - } - // if so, pick one at random and replace in the String - final String match = matcher.group(2); - final int numMatches = match.length(); - String server = null; - if (numMatches > 0) - { - int matchNum = (int) Math.floor(Math.random() * numMatches); - server = "" + match.charAt(matchNum); - } - final String result = matcher.group(1) + (server==null?"":server) + matcher.group(3); - return result; - } - /** * @return semicolon-separated list of all fields */ diff --git a/src/tim/prune/gui/map/SiteNameUtils.java b/src/tim/prune/gui/map/SiteNameUtils.java new file mode 100644 index 0000000..cf53f72 --- /dev/null +++ b/src/tim/prune/gui/map/SiteNameUtils.java @@ -0,0 +1,73 @@ +package tim.prune.gui.map; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper functions for manipulating tile site names + */ +public abstract class SiteNameUtils +{ + /** Regular expression for catching server wildcards */ + private static final Pattern WILD_PATTERN = Pattern.compile("^(.*)\\[(.*)\\](.*)$"); + + + /** + * If the base url contains something like [1234], then pick a server + * @param inBaseUrl base url + * @return modified base url + */ + public static String pickServerUrl(String inBaseUrl) + { + if (inBaseUrl == null || inBaseUrl.indexOf('[') < 0) { + return inBaseUrl; + } + // Check for [.*] (once only) + // Only need to support one, make things a bit easier + final Matcher matcher = WILD_PATTERN.matcher(inBaseUrl); + // if not, return base url unchanged + if (!matcher.matches()) { + return inBaseUrl; + } + // if so, pick one at random and replace in the String + final String match = matcher.group(2); + final int numMatches = match.length(); + String server = null; + if (numMatches > 0) + { + int matchNum = (int) Math.floor(Math.random() * numMatches); + server = "" + match.charAt(matchNum); + } + final String result = matcher.group(1) + (server==null?"":server) + matcher.group(3); + return result; + } + + + /** + * Fix the site name by stripping off protocol and www. + * This is used to create the file path for disk caching + * @param inUrl url to strip + * @return stripped url + */ + public static String convertUrlToDirectory(String inUrl) + { + if (inUrl == null || inUrl.equals("")) {return null;} + String url = inUrl.toLowerCase(); + int idx = url.indexOf("://"); + if (idx >= 0) {url = url.substring(idx + 3);} + if (url.startsWith("www.")) {url = url.substring(4);} + // Strip out any "[.*]" as well + if (url.indexOf('[') >= 0) + { + Matcher matcher = WILD_PATTERN.matcher(url); + if (matcher.matches()) + { + url = matcher.group(1) + matcher.group(3); + if (url.length() > 1 && url.charAt(0) == '.') { + url = url.substring(1); + } + } + } + return url; + } +} diff --git a/src/tim/prune/lang/prune-texts_af.properties b/src/tim/prune/lang/prune-texts_af.properties index 03acf63..8fab157 100644 --- a/src/tim/prune/lang/prune-texts_af.properties +++ b/src/tim/prune/lang/prune-texts_af.properties @@ -814,8 +814,6 @@ error.language.wrongfile=Die geselekteerde leer lyk nie soos 'n taal leer vir Gp error.convertnamestotimes.nonames=Geen name kon in tye omgeskakel word error.lookupsrtm.nonefound=Geen hoogte waardes beskikbaar vir die punte error.lookupsrtm.nonerequired=Al die punte het klaar hoogtes, so daar is niks meer om te soek -error.gpsies.uploadnotok=Die gpsies server het die boodskap terug gestuur -error.gpsies.uploadfailed=Die oplaai het misluk met die volgende fout boodskap error.showphoto.failed=Foto het nie gelaai nie error.playaudiofailed=Klankgreep het nie gespeel nie error.cache.notthere=Die te\u00ebl stoorarea gids kon nie opgespoor word nie diff --git a/src/tim/prune/lang/prune-texts_cz.properties b/src/tim/prune/lang/prune-texts_cz.properties index 92afbf4..39f177e 100644 --- a/src/tim/prune/lang/prune-texts_cz.properties +++ b/src/tim/prune/lang/prune-texts_cz.properties @@ -95,6 +95,7 @@ function.convertnamestotimes=P\u0159ev\u00e9st n\u00e1zvy v\u00fdzna\u010dn\u00f function.deletefieldvalues=Smazat hodnoty pole function.findwaypoint=Hledat bod function.pastecoordinates=Zadat sou\u0159adnice +function.enterpluscode=Zadat plus k\u00f3d function.charts=Grafy function.show3d=Trojrozm\u011brn\u011b function.distances=Vzd\u00e1lenosti @@ -814,8 +815,6 @@ error.language.wrongfile=Vybran\u00fd soubor nevypad\u00e1 jako jazykov\u00fd so error.convertnamestotimes.nonames=N\u00e1zvy nemohou b\u00fdt p\u0159evedeny na \u010dasov\u00e9 zna\u010dky error.lookupsrtm.nonefound=Pro tyto body nen\u00ed k dispozici informace o nadmo\u0159sk\u00e9 v\u00fd\u0161ce error.lookupsrtm.nonerequired=U v\u0161ech bod\u016f u\u017e je informaci o v\u00fd\u0161ce, tak\u017ee nen\u00ed co dohled\u00e1vat -error.gpsies.uploadnotok=Server gpsies vr\u00e1til hl\u00e1\u0161en\u00ed -error.gpsies.uploadfailed=Chyba - nepoda\u0159ilo se nahr\u00e1t data. error.showphoto.failed=Nepoda\u0159ilo se na\u010d\u00edst fotografii error.playaudiofailed=Nepoda\u0159ilo se p\u0159ehr\u00e1t zvukov\u00fd soubor. error.cache.notthere=Nepoda\u0159ilo se nal\u00e9zt adres\u00e1\u0159 s cache map. diff --git a/src/tim/prune/lang/prune-texts_de.properties b/src/tim/prune/lang/prune-texts_de.properties index ffc031d..1064211 100644 --- a/src/tim/prune/lang/prune-texts_de.properties +++ b/src/tim/prune/lang/prune-texts_de.properties @@ -876,8 +876,6 @@ error.language.wrongfile=Die ausgew\u00e4hlte Datei scheint keine Sprachdatei f\ error.convertnamestotimes.nonames=Es konnten keine Namen umgewandelt werden error.lookupsrtm.nonefound=Keine H\u00f6hendaten verf\u00fcgbar f\u00fcr diese Punkte error.lookupsrtm.nonerequired=Alle Punkte haben schon H\u00f6hendaten -error.gpsies.uploadnotok=Der Gpsies Server hat geantwortet -error.gpsies.uploadfailed=Das Hochladen ist fehlgeschlagen error.showphoto.failed=Das Foto konnte nicht geladen werden error.playaudiofailed=Das Abspielen der Audiodatei ist fehlgeschlagen error.cache.notthere=Der Ordner wurde nicht gefunden diff --git a/src/tim/prune/lang/prune-texts_de_CH.properties b/src/tim/prune/lang/prune-texts_de_CH.properties index 555390e..76771e0 100644 --- a/src/tim/prune/lang/prune-texts_de_CH.properties +++ b/src/tim/prune/lang/prune-texts_de_CH.properties @@ -871,8 +871,6 @@ error.language.wrongfile=Die uusgew\u00e4hlti Datei scheint kei Sproch-Datei f\u error.convertnamestotimes.nonames=Kei Namen han k\u00f6nnet verwondlet werde error.lookupsrtm.nonefound=Kei H\u00f6hendate verf\u00fcegbar f\u00fcr d'P\u00fcnkte error.lookupsrtm.nonerequired=Alle P\u00fcnkte han die H\u00f6hendate scho. N\u00fc\u00fct z'tue. -error.gpsies.uploadnotok=Der Gpsies Server h\u00e4t gseit gha -error.gpsies.uploadfailed=S Uufalade isch fehlgschlage error.showphoto.failed=S F\u00f6teli han i n\u00f6d k\u00f6nne lade error.playaudiofailed=S Abschpiele vonem File isch fehlgschlage error.cache.notthere=D Ordner isch n\u00f6d gfunde worde diff --git a/src/tim/prune/lang/prune-texts_en.properties b/src/tim/prune/lang/prune-texts_en.properties index ad0d8db..f2910cf 100644 --- a/src/tim/prune/lang/prune-texts_en.properties +++ b/src/tim/prune/lang/prune-texts_en.properties @@ -886,8 +886,6 @@ error.language.wrongfile=The selected file doesn't appear to be a language file error.convertnamestotimes.nonames=No names could be converted into times error.lookupsrtm.nonefound=No altitude values available for these points error.lookupsrtm.nonerequired=All points already have altitudes, so there's nothing to lookup -error.gpsies.uploadnotok=The gpsies server returned the message -error.gpsies.uploadfailed=The upload failed with the error error.showphoto.failed=Failed to load photo error.playaudiofailed=Failed to play audio clip error.cache.notthere=The tile cache directory was not found diff --git a/src/tim/prune/lang/prune-texts_es.properties b/src/tim/prune/lang/prune-texts_es.properties index 6d6b76e..9885d80 100644 --- a/src/tim/prune/lang/prune-texts_es.properties +++ b/src/tim/prune/lang/prune-texts_es.properties @@ -887,8 +887,6 @@ error.language.wrongfile=El archivo seleccionado no parece ser un archivo de len error.convertnamestotimes.nonames=Los nombres no pudieron ser convertidos en tiempos error.lookupsrtm.nonefound=No se encontraron valores de altitud error.lookupsrtm.nonerequired=Todos los puntos tienen altitudes, as\u00ed que no hay nada que buscar. -error.gpsies.uploadnotok=El servidor de gpsies ha devuelto el mensaje -error.gpsies.uploadfailed=La carga ha fallado con el error error.showphoto.failed=Fallo al cargar la foto error.playaudiofailed=Fallo reproduciendo archivo de audio error.cache.notthere=No se encontr\u00f3 la carpeta del cache de recuadros diff --git a/src/tim/prune/lang/prune-texts_fi.properties b/src/tim/prune/lang/prune-texts_fi.properties index 226a84b..2b7566e 100644 --- a/src/tim/prune/lang/prune-texts_fi.properties +++ b/src/tim/prune/lang/prune-texts_fi.properties @@ -100,6 +100,7 @@ function.pastecoordinates=Anna uudet koordinaatit... function.charts=Kaaviot function.show3d=3D-n\u00e4kym\u00e4 function.distances=V\u00e4limatkat +function.viewfulldetails=Kaikki yksityiskohdat function.estimatetime=Arvioi aika function.learnestimationparams=Opi aika-arvion parametrit function.autoplay=Animoi reitti @@ -514,6 +515,7 @@ dialog.diskcache.table.zoom=Zoom dialog.diskcache.table.tiles=Tiles dialog.diskcache.table.megabytes=Megabytes dialog.diskcache.tileset=Tileset +dialog.diskcache.tileset.multiple=usea dialog.diskcache.deleteold=Poista vanhan karttapalat dialog.diskcache.maximumage=Enimm\u00e4isik\u00e4 (p\u00e4ivi\u00e4) dialog.diskcache.deleteall=Poista kaikki kartapalat @@ -702,6 +704,7 @@ map.overzoom=Ei karttoja t\u00e4lle suurennussuhteelle # Field names fieldname.latitude=Leveysaste (Lat.) fieldname.longitude=Pituusaste (Long.) +fieldname.coordinates=Koordinaatit fieldname.altitude=Korkeus fieldname.timestamp=Aikaleima fieldname.time=Aika @@ -716,6 +719,7 @@ fieldname.duration=Kesto fieldname.speed=Nopeus fieldname.verticalspeed=Pystysuora nopeus fieldname.description=Kuvaus +fieldname.comment=Kommentti fieldname.mediafilename=Median tiedostonimi # Measurement units @@ -834,8 +838,6 @@ error.language.wrongfile=Valittu tiedosto ei n\u00e4yt\u00e4 olevan GpsPrune:n k error.convertnamestotimes.nonames=Kohdepisteiden nimi\u00e4 ei voitu muuntaa ajoiksi error.lookupsrtm.nonefound=N\u00e4iss\u00e4 pisteiss\u00e4 ei ole korkeustietoja error.lookupsrtm.nonerequired=Kaikilla pisteill\u00e4 on jo korkeustieto,\njoten ei ole mit\u00e4\u00e4n haettavaa -error.gpsies.uploadnotok='gpsies'-palvelin palautti viestin: -error.gpsies.uploadfailed=Tiedoston uppaus ep\u00e4onnistui virheilmoitukseen: error.showphoto.failed=Kuvan lataus ei onnistunut error.playaudiofailed=\u00c4\u00e4nileikkeen toisto ei onnistunut error.cache.notthere=Karttapalojen v\u00e4limuistihakemistoa ei l\u00f6ytynyt diff --git a/src/tim/prune/lang/prune-texts_fr.properties b/src/tim/prune/lang/prune-texts_fr.properties index 9b3efec..2d29392 100644 --- a/src/tim/prune/lang/prune-texts_fr.properties +++ b/src/tim/prune/lang/prune-texts_fr.properties @@ -878,8 +878,6 @@ error.language.wrongfile=Le fichier s\u00e9lectionn\u00e9 n'est pas un fichier d error.convertnamestotimes.nonames=Aucun nom n'a pu \u00eatre converti en horaire error.lookupsrtm.nonefound=Aucune valeur d'altitude trouv\u00e9e pour les points error.lookupsrtm.nonerequired=Tous les points ont d\u00e9j\u00e0 une altitude, il n'y a rien \u00e0 r\u00e9cup\u00e9rer -error.gpsies.uploadnotok=Le serveur de Gpsies a renvoy\u00e9 le message -error.gpsies.uploadfailed=L'envoi a \u00e9chou\u00e9 avec l'erreur error.showphoto.failed=Impossible de charger la photo error.playaudiofailed=\u00c9chec de la lecture du fichier audio error.cache.notthere=Le dossier du cache n'a pas \u00e9t\u00e9 trouv\u00e9 diff --git a/src/tim/prune/lang/prune-texts_hu.properties b/src/tim/prune/lang/prune-texts_hu.properties index 8239865..c3e9b1b 100644 --- a/src/tim/prune/lang/prune-texts_hu.properties +++ b/src/tim/prune/lang/prune-texts_hu.properties @@ -888,8 +888,6 @@ error.language.wrongfile=\u00dagy t\u0171nik, hogy a kiv\u00e1lasztott f\u00e1jl error.convertnamestotimes.nonames=A nevek nem konvert\u00e1lhat\u00f3k id\u0151adatokk\u00e1 error.lookupsrtm.nonefound=Nem \u00e9rhet\u0151 el magass\u00e1gi \u00e9rt\u00e9k ezekhez a pontokhoz error.lookupsrtm.nonerequired=Az \u00f6sszes pont m\u00e1r rendelkezik magass\u00e1gadatokkal, \u00edgy nincs mit keresni -error.gpsies.uploadnotok=A gpsies szerver a k\u00f6vetkez\u0151 \u00fczenetet adta vissza -error.gpsies.uploadfailed=A felt\u00f6lt\u00e9s nem siker\u00fclt a k\u00f6vetkez\u0151 hib\u00e1val error.showphoto.failed=F\u00e9nyk\u00e9p bet\u00f6lt\u00e9se sikertelen error.playaudiofailed=A hangf\u00e1jl lej\u00e1tsz\u00e1sa nem siker\u00fclt error.cache.notthere=A csempegyors\u00edt\u00f3t\u00e1r k\u00f6nyvt\u00e1ra nem tal\u00e1lhat\u00f3 diff --git a/src/tim/prune/lang/prune-texts_it.properties b/src/tim/prune/lang/prune-texts_it.properties index 2e7b3aa..38c4651 100644 --- a/src/tim/prune/lang/prune-texts_it.properties +++ b/src/tim/prune/lang/prune-texts_it.properties @@ -882,8 +882,6 @@ error.language.wrongfile=Il file selezionato non sembra essere un file di lingua error.convertnamestotimes.nonames=Nomi non convertibili in orari error.lookupsrtm.nonefound=Valori di quota non trovati error.lookupsrtm.nonerequired=Tutti i punti hanno gi\u00e0 una quota, non c'\u00e8 niente da cercare -error.gpsies.uploadnotok=Il server Gpsies ha riportato il messaggio -error.gpsies.uploadfailed=Il caricamento \u00e8 fallito con l'errore error.showphoto.failed=Caricamento foto fallito error.playaudiofailed=Ripresa audio non riprodotta error.cache.notthere=Directory del cache di tasselli non trovato diff --git a/src/tim/prune/lang/prune-texts_ja.properties b/src/tim/prune/lang/prune-texts_ja.properties index f698aba..28a9584 100644 --- a/src/tim/prune/lang/prune-texts_ja.properties +++ b/src/tim/prune/lang/prune-texts_ja.properties @@ -622,6 +622,4 @@ error.language.wrongfile=\u9078\u629e\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb\ error.convertnamestotimes.nonames=\u3069\u306e\u540d\u524d\u3082\u6642\u9593\u306b\u5909\u63db\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 error.lookupsrtm.nonefound=\u3069\u306e\u6a19\u9ad8\u5024\u3082\u3053\u308c\u3089\u306e\u30dd\u30a4\u30f3\u30c8\u304b\u3089\u53d6\u5f97\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002 error.lookupsrtm.nonerequired=\u5168\u3066\u306e\u30dd\u30a4\u30f3\u30c8\u306f\u6a19\u9ad8\u5024\u3092\u6301\u3063\u3066\u3044\u308b\u305f\u3081\u3001\u691c\u7d22\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002 -error.gpsies.uploadnotok=Gpsies\u30b5\u30fc\u30d0\u30fc\u304c\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u8fd4\u3057\u307e\u3057\u305f\u3002 -error.gpsies.uploadfailed=\u30a8\u30e9\u30fc\u306e\u305f\u3081\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f error.playaudiofailed=\u30aa\u30fc\u30c7\u30a3\u30aa\u30d5\u30a1\u30a4\u30eb\u306e\u518d\u751f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002 diff --git a/src/tim/prune/lang/prune-texts_ko.properties b/src/tim/prune/lang/prune-texts_ko.properties index 75332e4..d553df0 100644 --- a/src/tim/prune/lang/prune-texts_ko.properties +++ b/src/tim/prune/lang/prune-texts_ko.properties @@ -626,6 +626,4 @@ error.language.wrongfile=\uc120\ud0dd\ud558\uc2e0 \ud30c\uc77c\uc774 GpsPrune\uc error.convertnamestotimes.nonames=\uc774\ub984\uc744 \uc2dc\uac04\uc73c\ub85c \ubcc0\ud658 \ud560 \uc218 \uc5c6\uc5b4\uc694./n(\uacbd\uc720\uc9c0\uc774\ub984\uc744 \ucc3e\uc9c0 \ubabb\ud588\uac70\ub098 \ubcc0\ud658\ud560 \uc218 \uc5c6\ub294 \uacbd\uc6b0\uc5d0\uc694.) error.lookupsrtm.nonefound=\uc774 \uc9c0\uc810\ub4e4\uc5d0\uc11c \uace0\ub3c4\uac12\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc5b4\uc694./n\uc774 \uc601\uc5ed\uc5d0\uc11c \ud0c0\uc77c\uc774 \uc5c6\uac70\ub098, \uc790\ub8cc\uac00 \uc54c\uc218 \uc5c6\ub294 \uac83\ub4e4\uc744 \ud3ec\ud568\ud558\uace0 \uc788\ub294 \uacbd\uc720. error.lookupsrtm.nonerequired=\ubaa8\ub4e0 \uc9c0\uc810\uc774 \uace0\ub3c4 \uc815\ubcf4\uac00 \uc788\uc5b4\uc11c, \ucc3e\uc744\uac8c \uc544\ubb34\uac83\ub3c4 \uc5c6\ub124\uc694. -error.gpsies.uploadnotok=gpsies \uc11c\ubc84\uac00 \uba54\uc138\uc9c0\ub97c \ub2e4\uc74c\uacfc \uac19\uc740 \ub3cc\ub824\uc90d\ub2c8\ub2e4 -error.gpsies.uploadfailed=\ub2e4\uc74c\uacfc \uac19\uc740 \uc774\uc720\ub85c \uc5c5\ub85c\ub4dc\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4 error.playaudiofailed=\uc18c\ub9ac\ud30c\uc77c \uc7ac\uc0dd \uc2e4\ud328 diff --git a/src/tim/prune/lang/prune-texts_nl.properties b/src/tim/prune/lang/prune-texts_nl.properties index c757ccf..009d248 100644 --- a/src/tim/prune/lang/prune-texts_nl.properties +++ b/src/tim/prune/lang/prune-texts_nl.properties @@ -885,8 +885,6 @@ error.language.wrongfile=Het geselecteerde bestand is vermoedelijk geen taalbest error.convertnamestotimes.nonames=Namen konden niet naar tijden worden geconverteerd error.lookupsrtm.nonefound=Geen hoogtewaarden beschikbaar voor deze punten error.lookupsrtm.nonerequired=Alle punten hebben reeds hoogte, er hoeft niets te worden opgezocht. -error.gpsies.uploadnotok=Gpsies server antwoordde met -error.gpsies.uploadfailed=De upload is mislukt. Fout error.showphoto.failed=Foto laden mislukt error.playaudiofailed=Kon audiobestand niet afspelen error.cache.notthere=De tegelcache map niet gevonden diff --git a/src/tim/prune/lang/prune-texts_pl.properties b/src/tim/prune/lang/prune-texts_pl.properties index 75bfc80..2733749 100644 --- a/src/tim/prune/lang/prune-texts_pl.properties +++ b/src/tim/prune/lang/prune-texts_pl.properties @@ -97,6 +97,7 @@ function.convertnamestotimes=Zamie\u0144 nazwy punkt\u00f3w na czas function.deletefieldvalues=Usu\u0144 warto\u015bci function.findwaypoint=Znajd\u017a punkt po\u015bredni function.pastecoordinates=Wprowad\u017a nowe wsp\u00f3\u0142rz\u0119dne +function.enterpluscode=Wpisz kod plus function.charts=Wykres function.show3d=Poka\u017c model 3D function.distances=Odleg\u0142o\u015bci @@ -117,6 +118,7 @@ function.searchopencachingde=Szukaj w OpenCaching.de function.mapillary=Szukaj zdj\u0119cia w Mapillary function.downloadosm=Za\u0142aduj dane obszaru z OSM function.duplicatepoint=Duplikuj plik +function.projectpoint=Rzucaj plik function.setcolours=Ustaw kolory function.setdisplaysettings=Ustawienia wy\u015bwietlacza function.setlanguage=Zmie\u0144 j\u0119zyk @@ -369,6 +371,8 @@ dialog.wikipedia.column.distance=Odleg\u0142o\u015b\u0107 dialog.wikipedia.nonefound=Brak wpis\u00f3w w wikipedii dialog.wikipedia.gallery=Galeria dialog.osmpois.column.name=Nazwa +dialog.osmpois.column.type=Typ +dialog.osmpois.nonefound=Nie znaleziono punkt\u00f3w dialog.geocaching.nonefound=Nic nie zosta\u0142o znalezione dialog.correlate.notimestamps=Punkty nie maj\u0105 znacznik\u00f3w czasu, nie mo\u017cna ich powi\u0105za\u0107 ze zdj\u0119ciami. dialog.correlate.nouncorrelatedphotos=Nie ma nie powi\u0105zanych zdj\u0119\u0107.\nCzy na pewno chcesz kontynuowa\u0107? @@ -525,11 +529,16 @@ dialog.diskcache.deleteall=Usu\u0144 wszystkie p\u0142ytki dialog.diskcache.deleted=Usuni\u0119to %d plik\u00f3w z kesza dialog.deletefieldvalues.intro=Wybierz pola do skasowania z wybranego zakresu dialog.deletefieldvalues.nofields=Brak p\u00f3l do skasowania dla tego zakresu +dialog.displaysettings.waypointicons=Ikony dla punkt\u00f3w +dialog.displaysettings.wpicon.plectrum=Plektron +dialog.displaysettings.wpicon.ring=Pier\u015Bcie\u0144 dialog.displaysettings.linewidth=Wprowad\u017a grubo\u015b\u0107 linii do rysowania \u015bcie\u017cek dialog.displaysettings.antialias=U\u017Cyj antyaliasingu dialog.displaysettings.size.small=Ma\u0142e dialog.displaysettings.size.medium=\u015arednie dialog.displaysettings.size.large=Du\u017ce +dialog.displaysettings.windowstyle=Styl okna (wymaga to restartu) +dialog.displaysettings.windowstyle.default=Domy\u015Blny dialog.downloadosm.desc=Potwierd\u017a \u015bci\u0105gni\u0119cie danych dla tego obszaru z OSM: dialog.searchwikipedianames.search=Szukaj dialog.weather.location=Pozycja @@ -561,12 +570,14 @@ dialog.deletebydate.column.keep=Zostaw dialog.deletebydate.column.delete=Usu\u0144 dialog.setaltitudetolerance.text.metres=Limit (w metrach) poni\u017cej kt\u00f3rego, ma\u0142e spadki wzniosy b\u0119d\u0105 ignorowane dialog.setaltitudetolerance.text.feet=Limit (w stopach) poni\u017cej kt\u00f3rego, ma\u0142e spadki wzniosy b\u0119d\u0105 ignorowane +dialog.settimezone.system=Stref\u0119 czasow\u0105 systemu dialog.settimezone.selectedzone=Wybrana strefa czasowa dialog.autoplay.duration=Czas trwania (sek) dialog.autoplay.usetimestamps=U\u017Cyj znacznik\u00f3w czasowych dialog.autoplay.rewind=Przewin\u0105\u0107 dialog.autoplay.pause=Pauza dialog.autoplay.play=Graj +dialog.projectpoint.bearing=Kierunek (stopnie od p\u00f3\u0142nocy) # 3d window dialog.3d.title=GpsPrune widok tr\u00f3jwymiarowy @@ -831,6 +842,7 @@ error.load.nopoints=Nie znaleziono informacji o wsp\u00f3\u0142rz\u0119dnych w p error.load.unknownxml=Nieznany format xml: error.load.noxmlinzip=Nie znaleziono pliku xml w pliku zip error.load.othererror=B\u0142\u0105d czytania pliku: +error.load.nopointsintext=Nie znaleziono wsp\u00f3\u0142rz\u0119dnych error.jpegload.dialogtitle=B\u0142\u0105d \u0142adowania zdj\u0119cia error.jpegload.nofilesfound=Nie znaleziono plik\u00f3w error.jpegload.nojpegsfound=Nie znaleziono plik\u00f3w jpeg @@ -851,8 +863,6 @@ error.language.wrongfile=Wybrany plik nie jest plikiem z t\u0142umaczeniem dla G error.convertnamestotimes.nonames=\u017badne nazwy nie mog\u0142y zosta\u0107 zmienione na czas error.lookupsrtm.nonefound=Nie znaleziono danych o wysoko\u015bci. error.lookupsrtm.nonerequired=Wszystkie pola maj\u0105 informacj\u0119 o wysoko\u015bci, nie ma czego szuka\u0107 -error.gpsies.uploadnotok=Serwer Gpsies zwr\u00f3ci\u0142 informacj\u0119 -error.gpsies.uploadfailed=B\u0142\u0105d wysy\u0142ania error.showphoto.failed=Nie powiod\u0142o si\u0119 za\u0142adowanie zdj\u0119cia error.playaudiofailed=Nie powiod\u0142o si\u0119 odtwarzanie pliku audio error.cache.notthere=Nie znaleziono katalogu kesza diff --git a/src/tim/prune/lang/prune-texts_pt.properties b/src/tim/prune/lang/prune-texts_pt.properties index 82fc087..8190965 100644 --- a/src/tim/prune/lang/prune-texts_pt.properties +++ b/src/tim/prune/lang/prune-texts_pt.properties @@ -96,6 +96,7 @@ function.convertnamestotimes=Converter nomes dos pontos para tempos function.deletefieldvalues=Remover valores do campo function.findwaypoint=Encontrar ponto function.pastecoordinates=Inserir novas coordenadas +function.enterpluscode=Inserir um plus code function.charts=Gr\u00e1ficos function.show3d=Visualizar 3D function.distances=Dist\u00e2ncias @@ -821,8 +822,6 @@ error.language.wrongfile=O arquivo selecionado n\u00e3o parece ser um arquivo de error.convertnamestotimes.nonames=Nenhum nome pode ser convertido para tempo error.lookupsrtm.nonefound=Nenhum valor de altitude encontrado error.lookupsrtm.nonerequired=Todos os pontos j\u00e1 possuem altitude, assim n\u00e3o h\u00e1 nada a procurar -error.gpsies.uploadnotok=O servidor Gpsies retornou a mensagem -error.gpsies.uploadfailed=O envio falhou com o erro error.showphoto.failed=Falha ao carregar foto error.playaudiofailed=Falha ao reproduzir arquivo de \u00e1udio error.cache.notthere=A paste de cache de fundos n\u00e3o foi encontrada diff --git a/src/tim/prune/lang/prune-texts_ro.properties b/src/tim/prune/lang/prune-texts_ro.properties index e3a5610..7affd24 100644 --- a/src/tim/prune/lang/prune-texts_ro.properties +++ b/src/tim/prune/lang/prune-texts_ro.properties @@ -851,8 +851,6 @@ error.language.wrongfile=Fi\u0219ierul selectat nu pare a fi un fi\u0219ier de l error.convertnamestotimes.nonames=Niciun nume de waypoint nu a fost g\u0103sit sau nu au putut fi convertite error.lookupsrtm.nonefound=Pentru aceste puncte nu exist\u0103 valori de altitudine error.lookupsrtm.nonerequired=Toate punctele au deja altitudine, nu e nimic de calculat -error.gpsies.uploadnotok=Server-ul gpsies a \u00eentors mesajul -error.gpsies.uploadfailed=Upload-ul a e\u0219uat cu eroarea error.showphoto.failed=\u00cenc\u0103rcarea foto a e\u0219uat error.playaudiofailed=\u00cencercarea de a reda clipul audio a e\u0219uat error.cache.notthere=Directorul tile cache nu a fost g\u0103sit diff --git a/src/tim/prune/lang/prune-texts_ru.properties b/src/tim/prune/lang/prune-texts_ru.properties index b1fe180..82a8fa9 100644 --- a/src/tim/prune/lang/prune-texts_ru.properties +++ b/src/tim/prune/lang/prune-texts_ru.properties @@ -883,8 +883,6 @@ error.language.wrongfile=\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 error.convertnamestotimes.nonames=\u041d\u0435\u0442 \u0438\u043c\u0435\u043d, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u0432\u043e \u0432\u0440\u0435\u043c\u044f error.lookupsrtm.nonefound=\u041d\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043e\u0442\u043c\u0435\u0442\u043e\u043a \u0432\u044b\u0441\u043e\u0442 \u0434\u043b\u044f \u044d\u0442\u0438\u0445 \u0442\u043e\u0447\u0435\u043a. error.lookupsrtm.nonerequired=\u0412\u0441\u0435 \u0442\u043e\u0447\u043a\u0438 \u0443\u0436\u0435 \u0438\u043c\u0435\u044e\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432\u044b\u0441\u043e\u0442, \u043d\u0435\u0447\u0435\u0433\u043e \u043f\u0440\u043e\u0441\u043c\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044c -error.gpsies.uploadnotok=Gpsies \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 -error.gpsies.uploadfailed=\u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0430 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439 error.showphoto.failed=\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u043e\u0442\u043e error.playaudiofailed=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u0437\u0432\u0443\u043a\u043e\u0437\u0430\u043f\u0438\u0441\u0438 error.cache.notthere=\u041f\u0430\u043f\u043a\u0430 \u043a\u044d\u0448\u0430 \u0441 \u0442\u0430\u0439\u043b\u0430\u043c\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 diff --git a/src/tim/prune/lang/prune-texts_sv.properties b/src/tim/prune/lang/prune-texts_sv.properties index 6192c5c..4e46980 100644 --- a/src/tim/prune/lang/prune-texts_sv.properties +++ b/src/tim/prune/lang/prune-texts_sv.properties @@ -167,6 +167,7 @@ dialog.openoptions.filesnippet=Filextrakt dialog.load.table.field=F\u00e4lt dialog.load.table.datatype=Datatyp dialog.load.table.description=Beskrivning +dialog.delimiter.label=Avgr\u00e4nsare dialog.delimiter.comma=Komma , dialog.delimiter.tab=Tabb dialog.delimiter.space=Mellanslag @@ -715,6 +716,7 @@ display.range.time.days=d details.range.avespeed=Medelhastighet details.range.maxspeed=Maxhastighet details.range.numsegments=Antal segment +details.range.pace=Tempo details.range.gradient=Gradient details.lists.waypoints=Waypoints details.lists.photos=Foton @@ -742,8 +744,10 @@ fieldname.date=Datum fieldname.waypointname=Namn fieldname.waypointtype=Typ fieldname.newsegment=Segment +fieldname.custom=Anpassad fieldname.prefix=F\u00e4lt fieldname.distance=Distans +fieldname.duration=Varaktighet fieldname.speed=Hastighet fieldname.verticalspeed=Vertikal hastighet fieldname.description=Beskrivning @@ -855,3 +859,5 @@ error.jpegload.nogpsfound=Ingen GPS-information hittades error.audioload.nofilesfound=Inga ljud-klipp hittades error.gpsload.unknown=Ok\u00e4nt fel error.undofailed.title=\u00c5ngra misslyckades +error.function.notavailable.title=Funktionen \u00e4r inte tillg\u00e4nglig +error.readme.notfound=Ingen L\u00e4sMig-fil hittades diff --git a/src/tim/prune/lang/prune-texts_zh.properties b/src/tim/prune/lang/prune-texts_zh.properties index d915629..fb68fb2 100644 --- a/src/tim/prune/lang/prune-texts_zh.properties +++ b/src/tim/prune/lang/prune-texts_zh.properties @@ -821,8 +821,6 @@ error.language.wrongfile=\u6240\u9009\u8bed\u8a00\u5305\u6587\u4ef6\u683c\u5f0f\ error.convertnamestotimes.nonames=\u540d\u79f0\u65e0\u6cd5\u8f6c\u6362\u6210\u65f6\u95f4 error.lookupsrtm.nonefound=\u65e0\u9ad8\u5ea6\u4fe1\u606f error.lookupsrtm.nonerequired=\u6240\u6709\u70b9\u5747\u542b\u9ad8\u5ea6\u4fe1\u606f -error.gpsies.uploadnotok=gpsies\u670d\u52a1\u5668\u8fd4\u56de\u4fe1\u606f\uff1a -error.gpsies.uploadfailed=\u4e0a\u4f20\u5931\u8d25 error.showphoto.failed=\u52a0\u8f7d\u7167\u7247\u5931\u8d25 error.playaudiofailed=\u65e0\u6cd5\u64ad\u653e\u58f0\u97f3\u6587\u4ef6 error.cache.notthere=\u672a\u627e\u5230\u533a\u57df\u6570\u636e\u7f13\u5b58\u6587\u4ef6\u5939 diff --git a/src/tim/prune/load/BabelFileFormats.java b/src/tim/prune/load/BabelFileFormats.java index 5c54dfb..73c40ac 100644 --- a/src/tim/prune/load/BabelFileFormats.java +++ b/src/tim/prune/load/BabelFileFormats.java @@ -100,6 +100,7 @@ public abstract class BabelFileFormats "GeocachingDB for Palm/OS", "gcdb", null, "Geogrid-Viewer ascii overlay file", "ggv_ovl", ".ovl", "Geogrid-Viewer tracklogs", "ggv_log", ".log", + "GeoJSON", "geojson", ".json", "GEOnet Names Server (GNS)", "geonet", null, "GlobalSat DG-100/BT-335", "dg-100", null, "GlobalSat DG-200", "dg-200", null, diff --git a/src/tim/prune/load/FileLoader.java b/src/tim/prune/load/FileLoader.java index 52bdef2..ba11005 100644 --- a/src/tim/prune/load/FileLoader.java +++ b/src/tim/prune/load/FileLoader.java @@ -10,6 +10,7 @@ import javax.swing.JFrame; import tim.prune.App; import tim.prune.config.Config; import tim.prune.data.Photo; +import tim.prune.load.json.JsonFileLoader; import tim.prune.load.xml.GzipFileLoader; import tim.prune.load.xml.XmlFileLoader; import tim.prune.load.xml.ZipFileLoader; @@ -142,6 +143,10 @@ public class FileLoader _app.informPhotosLoaded(photoSet); _app.informNoDataLoaded(); // To trigger load of next file if any } + else if (fileExtension.equals("json")) + { + new JsonFileLoader(_app).openFile(inFile); + } else { // Use text loader for everything else diff --git a/src/tim/prune/load/json/JsonBlock.java b/src/tim/prune/load/json/JsonBlock.java new file mode 100644 index 0000000..83486c7 --- /dev/null +++ b/src/tim/prune/load/json/JsonBlock.java @@ -0,0 +1,227 @@ +package tim.prune.load.json; + +import java.util.ArrayList; + +/** + * Structure to hold the contents of a Json block during parsing. + * This will be held within the current [] or {} block + */ +public class JsonBlock +{ + private String _latitude = null; + private String _longitude = null; + private String _altitude = null; + + private Expectation _nextField = Expectation.NONE; + private BlockType _type = BlockType.NONE; + private boolean _hasNonNumbers = false; + private ArrayList _pointCoords = null; + private ArrayList> _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(); + } + _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>(); + } + _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>(); + } + _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 pointCoords = _coordList.get(inIndex); + if (pointCoords.size() < 2 || pointCoords.size() > 3) {return null;} + final String longitude = pointCoords.get(0); + final String latitude = pointCoords.get(1); + final String altitude = ((pointCoords.size() == 3) ? pointCoords.get(3) : null); + return new JsonPoint(latitude, longitude, altitude, inIndex == 0); + } +} diff --git a/src/tim/prune/load/json/JsonFileLoader.java b/src/tim/prune/load/json/JsonFileLoader.java new file mode 100644 index 0000000..e1df666 --- /dev/null +++ b/src/tim/prune/load/json/JsonFileLoader.java @@ -0,0 +1,170 @@ +package tim.prune.load.json; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Stack; + +import tim.prune.App; +import tim.prune.data.Field; +import tim.prune.data.SourceInfo; + + +/** + * Class to handle the loading of GeoJSON files + */ +public class JsonFileLoader +{ + /** App for callback of file loading */ + private App _app = null; + /** Stack of blocks */ + private Stack _jsonBlocks = null; + /** List of points extracted */ + private ArrayList _jsonPoints = null; + private boolean _newSegment = true; + + + /** + * Constructor + * @param inApp App object + */ + public JsonFileLoader(App inApp) + { + _app = inApp; + _jsonBlocks = new Stack(); + _jsonPoints = new ArrayList(); + } + + /** + * 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 + * 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. + *

+ * 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. + *

+ * 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. + *

+ * The actions can be reversed using the REVERSE_ACTION + * constructor flags. The default action moves the ViewPlatform around the + * objects in the scene. The REVERSE_ACTION 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 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 not call this method. + */ + @Override + public void setViewingPlatform(ViewingPlatform vp) + { + super.setViewingPlatform( vp ); + + if (vp != null) { + resetView(); + integrateTransforms(); + } + } + + /** + * Reset the orientation and distance of this behaviour to the current + * values in the ViewPlatform Transform Group + */ + private void resetView() + { + Vector3d centerToView = new Vector3d(); + + targetTG.getTransform( targetTransform ); + + targetTransform.get( _rotMatrix, _transVector ); + centerToView.sub( _transVector, _rotationCenter ); + _distanceFromCenter = centerToView.length(); + _startDistanceFromCenter = _distanceFromCenter; + + targetTransform.get( _rotMatrix ); + _rotateTransform.set( _rotMatrix ); + + // compute the initial x/y/z offset + _temp1.set(centerToView); + _rotateTransform.invert(); + _rotateTransform.mul(_temp1); + _rotateTransform.get(centerToView); + _xtrans = centerToView.x; + _ytrans = centerToView.y; + _ztrans = centerToView.z; + + // reset rotMatrix + _rotateTransform.set( _rotMatrix ); + } + + protected synchronized void integrateTransforms() + { + // Check if the transform has been changed by another behaviour + Transform3D currentXfm = new Transform3D(); + targetTG.getTransform(currentXfm); + if (! targetTransform.equals(currentXfm)) + resetView(); + + // Three-step rotation process, firstly undo the pitch and apply the delta yaw + _latitudeTransform.rotX(_pitchAngle); + _rotateTransform.mul(_rotateTransform, _latitudeTransform); + _longitudeTransform.rotY( _deltaYaw ); + _rotateTransform.mul(_rotateTransform, _longitudeTransform); + // Now update pitch angle according to delta and apply + _pitchAngle = Math.min(Math.max(0.0, _pitchAngle - _deltaPitch), Math.PI/2.0); + _latitudeTransform.rotX(-_pitchAngle); + _rotateTransform.mul(_rotateTransform, _latitudeTransform); + + _distanceVector.z = _distanceFromCenter - _startDistanceFromCenter; + + _temp1.set(_distanceVector); + _temp1.mul(_rotateTransform, _temp1); + + // want to look at rotationCenter + _transVector.x = _rotationCenter.x + _xtrans; + _transVector.y = _rotationCenter.y + _ytrans; + _transVector.z = _rotationCenter.z + _ztrans; + + _translation.set(_transVector); + targetTransform.mul(_temp1, _translation); + + // handle rotationCenter + _temp1.set(_centerVector); + _temp1.mul(targetTransform); + + _invertCenterVector.x = -_centerVector.x; + _invertCenterVector.y = -_centerVector.y; + _invertCenterVector.z = -_centerVector.z; + + _temp2.set(_invertCenterVector); + targetTransform.mul(_temp1, _temp2); + + Vector3d finalTranslation = new Vector3d(); + targetTransform.get(finalTranslation); + + targetTG.setTransform(targetTransform); + + // reset yaw and pitch deltas + _deltaYaw = 0.0; + _deltaPitch = 0.0; + } + + private boolean isRotateEvent(MouseEvent evt) + { + final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0; + return !evt.isAltDown() && !isRightDrag; + } + + private boolean isZoomEvent(MouseEvent evt) + { + if (evt instanceof java.awt.event.MouseWheelEvent) { + return true; + } + return evt.isAltDown() && !evt.isMetaDown(); + } + + private boolean isTranslateEvent(MouseEvent evt) + { + final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0; + return !evt.isAltDown() && isRightDrag; + } +} diff --git a/test/tim/prune/data/RangeStatsTest.java b/test/tim/prune/data/RangeStatsTest.java new file mode 100644 index 0000000..d068ac1 --- /dev/null +++ b/test/tim/prune/data/RangeStatsTest.java @@ -0,0 +1,197 @@ +package tim.prune.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * JUnit tests for calculation of moving time of a range + * based on different timestamp availability + * @author fperrin + */ +class RangeStatsTest +{ + @Test + void movingTime() + { + Track track = new Track(); + DataPoint[] points = { + new DataPoint( + new String[] { + "01-Jan-2020 00:00:00", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:00:05", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:00:07", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + }; + track.appendPoints(points); + + RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1); + assertEquals(7, range.getMovingDurationInSeconds()); + assertEquals(7, range.getTotalDurationInSeconds()); + assertFalse(range.getTimestampsIncomplete()); + assertFalse(range.getTimestampsOutOfSequence()); + } + + @Test + void movingTimeWithGap() + { + Track track = new Track(); + DataPoint[] points = { + new DataPoint( + new String[] { + "01-Jan-2020 00:00:00", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + new DataPoint( + new String[] {}, + new FieldList(new Field[] {}), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:00:05", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:00:07", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + }; + track.appendPoints(points); + + RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1); + assertEquals(7, range.getMovingDurationInSeconds()); + assertEquals(7, range.getTotalDurationInSeconds()); + assertTrue(range.getTimestampsIncomplete()); + assertFalse(range.getTimestampsOutOfSequence()); + } + + @Test + void movingTimeSeveralSegments() + { + Track track = new Track(); + DataPoint[] points = { + new DataPoint( + new String[] { + "01-Jan-2020 00:01:00", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + new DataPoint( + new String[] {}, + new FieldList(new Field[] {}), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:01:05", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:01:07", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + // start a second segment + new DataPoint( + new String[] { + "01-Jan-2020 00:00:20", + "1", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + Field.NEW_SEGMENT, + }), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:00:27", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + }; + track.appendPoints(points); + + RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1); + assertEquals(7 + 7, range.getMovingDurationInSeconds()); + assertEquals(47, range.getTotalDurationInSeconds()); + assertTrue(range.getEarliestTimestamp().isEqual(new TimestampUtc("01-Jan-2020 00:00:20"))); + assertTrue(range.getLatestTimestamp().isEqual(new TimestampUtc("01-Jan-2020 00:01:07"))); + assertTrue(range.getTimestampsIncomplete()); + + // even though segment 2 is earlier than segment 1, timestamps + // within each segment are normally ordered + assertFalse(range.getTimestampsOutOfSequence()); + } + + @Test + void movingTimeMissingFirstTimestamp() + { + Track track = new Track(); + DataPoint[] points = { + new DataPoint( + new String[] {}, + new FieldList(new Field[] {}), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:00:00", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + new DataPoint( + new String[] { + "01-Jan-2020 00:00:05", + }, + new FieldList(new Field[] { + Field.TIMESTAMP, + }), + null), + }; + track.appendPoints(points); + + RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1); + assertEquals(5, range.getMovingDurationInSeconds()); + assertEquals(5, range.getTotalDurationInSeconds()); + assertTrue(range.getTimestampsIncomplete()); + assertFalse(range.getTimestampsOutOfSequence()); + } +} diff --git a/test/tim/prune/function/cache/TileSetTest.java b/test/tim/prune/function/cache/TileSetTest.java new file mode 100644 index 0000000..4ff108c --- /dev/null +++ b/test/tim/prune/function/cache/TileSetTest.java @@ -0,0 +1,44 @@ +package tim.prune.function.cache; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * JUnit tests for tile name checks + */ +class TileSetTest +{ + @Test + void testIsNumeric() + { + // not numeric, should be false + assertFalse(TileSet.isNumeric(null)); + assertFalse(TileSet.isNumeric("")); + assertFalse(TileSet.isNumeric("a")); + assertFalse(TileSet.isNumeric(" ")); + assertFalse(TileSet.isNumeric("155a")); + assertFalse(TileSet.isNumeric("-2")); + // numeric, should be true + assertTrue(TileSet.isNumeric("1")); + assertTrue(TileSet.isNumeric("155")); + } + + @Test + void testIsNumericUntilDot() + { + // not numeric, should be false + assertFalse(TileSet.isNumericUntilDot(null)); + assertFalse(TileSet.isNumericUntilDot("")); + assertFalse(TileSet.isNumericUntilDot(".")); + assertFalse(TileSet.isNumericUntilDot(".abc")); + assertFalse(TileSet.isNumericUntilDot("a3.")); + assertFalse(TileSet.isNumericUntilDot("4a")); + assertFalse(TileSet.isNumericUntilDot("215327h.png")); + // numeric but no dot, should be false + assertFalse(TileSet.isNumericUntilDot("1234")); + // numeric, should be true + System.out.println(TileSet.isNumericUntilDot("44.jpg")); + System.out.println(TileSet.isNumericUntilDot("0.")); + } +} diff --git a/test/tim/prune/function/olc/OlcDecoderTest.java b/test/tim/prune/function/olc/OlcDecoderTest.java new file mode 100644 index 0000000..951a081 --- /dev/null +++ b/test/tim/prune/function/olc/OlcDecoderTest.java @@ -0,0 +1,75 @@ +package tim.prune.function.olc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * JUnit tests for decoding of Open Location Codes (Pluscodes) + */ +class OlcDecoderTest +{ + + @Test + void testDecodeStringsTooShort() + { + OlcArea area = OlcDecoder.decode(null); + assertEquals(area, null, "Decoding null gives null"); + area = OlcDecoder.decode(""); + assertEquals(area, null, "Decoding \"\" gives null"); + area = OlcDecoder.decode("9"); + assertEquals(area, null, "Decoding \"9\" gives null"); + area = OlcDecoder.decode("9999999"); + assertEquals(area, null, "Decoding \"9999999\" gives null"); + } + + @Test + void testDecodeStringsInvalid() + { + OlcArea area = OlcDecoder.decode("11111111"); + assertEquals(area, null, "Decoding lots of 1s gives null"); + area = OlcDecoder.decode("99999991"); + assertEquals(area, null, "Decoding with a single 1 gives null"); + area = OlcDecoder.decode("99999999"); + assertNotEquals(area, null, "Decoding with all 9s gives non-null"); + area = OlcDecoder.decode("00000000"); + assertEquals(area, null, "Decoding with all padding gives null"); + area = OlcDecoder.decode("99000000"); + assertNotEquals(area, null, "Decoding with some padding gives non-null"); + } + + @Test + void testDecodeZeroes() + { + OlcArea area = OlcDecoder.decode("22000000"); + assertNotEquals(area, null, "Decoding with padding gives non-null"); + assertEquals(-90.0, area.minLat, 0.0, "South 90"); + assertEquals(-70.0, area.maxLat, 0.0, "South 70"); + assertEquals(-180.0, area.minLon, 0.0, "West 180"); + assertEquals(-160.0, area.maxLon, 0.0, "West 160"); + } + + @Test + void testDecodeZeroes2() + { + OlcArea area = OlcDecoder.decode("22220000"); + assertNotEquals(area, null, "Decoding with padding gives non-null"); + assertEquals(-90.0, area.minLat, 0.0, "South 90"); + assertEquals(-89.0, area.maxLat, 0.0, "South 89"); + assertEquals(-180.0, area.minLon, 0.0, "West 180"); + assertEquals(-179.0, area.maxLon, 0.0, "West 179"); + } + + @Test + void testMountainView() + { + OlcArea area = OlcDecoder.decode("6PH57VP3+PR6"); + assertNotEquals(area, null, "Decoding with separator gives non-null"); + System.out.println("Min lat: " + area.minLat); + System.out.println("Max lat: " + area.maxLat); + System.out.println("Min lon: " + area.minLon); + System.out.println("Max lon: " + area.maxLon); + assertTrue(area.maxLat > area.minLat, "latitude range"); + assertTrue(area.maxLon > area.minLon, "longitude range"); + } +} diff --git a/test/tim/prune/function/weather/SingleForecastTest.java b/test/tim/prune/function/weather/SingleForecastTest.java new file mode 100644 index 0000000..372ab47 --- /dev/null +++ b/test/tim/prune/function/weather/SingleForecastTest.java @@ -0,0 +1,52 @@ +package tim.prune.function.weather; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * JUnit tests for weather icons + */ +class SingleForecastTest +{ + + @Test + void testWeatherIcons() + { + testIconName(null, "100", ""); + testIconName("storm.png", "200", null); + testIconName("storm.png", "204", ""); + testIconName("lightrain.png", "300", null); + testIconName("lightrain.png", "301", null); + testIconName(null, "400", null); + testIconName("lightrain.png", "500", null); + testIconName("rain.png", "501", null); + testIconName("rain.png", "599", null); + testIconName("hail.png", "511", null); + testIconName("snow.png", "600", null); + testIconName("fog.png", "700", null); + testIconName("clear-day.png", "800", null); + testIconName("clear-day.png", "800", ""); + testIconName("clear-day.png", "800", "01d"); + testIconName("clear-night.png", "800", "01n"); + testIconName("clouds-day.png", "802", "01d"); + testIconName("clouds-night.png", "802", "01n"); + testIconName("clouds.png", "804", "01n"); + testIconName("extreme.png", "900", "01d"); + testIconName("hail.png", "906", "01n"); + } + + /** + * Test getting an icon name according to code and image + */ + private static void testIconName(String inExpect, String inCode, String inImage) + { + String icon = SingleForecast.getIconName(inCode, inImage); + assertEquals(inExpect, icon, showString(inCode) + ", " + showString(inImage)); + } + + private static String showString(String inString) + { + return inString == null ? "null" : inString; + } +} diff --git a/test/tim/prune/gui/map/MapSourceTest.java b/test/tim/prune/gui/map/MapSourceTest.java new file mode 100644 index 0000000..ff947c3 --- /dev/null +++ b/test/tim/prune/gui/map/MapSourceTest.java @@ -0,0 +1,31 @@ +package tim.prune.gui.map; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * JUnit tests for manipulating base Urls + */ +class MapSourceTest +{ + @Test + void testFixBaseUrls() + { + // Should succeed + testUrlFix("8bitcities.s3.amazonaws.com", "http://8bitcities.s3.amazonaws.com/"); + testUrlFix("8bitcities.s3.amazonaws.com/", "http://8bitcities.s3.amazonaws.com/"); + testUrlFix("http://8bitcities.s3.amazonaws.com/", "http://8bitcities.s3.amazonaws.com/"); + testUrlFix("something.com/ok", "http://something.com/ok/"); + + // These should fail and return null + testUrlFix("something/wrong", null); + testUrlFix("protocol://something.com/16/", null); + } + + private void testUrlFix(String inStart, String inExpected) + { + String result = MapSource.fixBaseUrl(inStart); + assertEquals(inExpected, result); + } +} diff --git a/test/tim/prune/gui/map/SiteNameUtilsTest.java b/test/tim/prune/gui/map/SiteNameUtilsTest.java new file mode 100644 index 0000000..e01ed52 --- /dev/null +++ b/test/tim/prune/gui/map/SiteNameUtilsTest.java @@ -0,0 +1,63 @@ +package tim.prune.gui.map; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashSet; + +import org.junit.jupiter.api.Test; + +/** + * JUnit tests for site name utils + */ +class SiteNameUtilsTest +{ + + @Test + void testPickServerNameWithoutWildcards() + { + testPickSingleUrl("abc", "abc"); + testPickSingleUrl("ab[]c", "abc"); + testPickSingleUrl("[]abc", "abc"); + testPickSingleUrl("abc[]", "abc"); + } + + /** + * Test a pattern without wildcards which should always produce the expected result + * @param inPattern pattern for site name + * @param inExpected expected resolved name + */ + private void testPickSingleUrl(String inPattern, String inExpected) + { + for (int i=0; i<20; i++) + { + String resolved = SiteNameUtils.pickServerUrl(inPattern); + assertEquals(inExpected, resolved, "Failed: " + inPattern); + } + } + + @Test + void testPickUsingWildcards() + { + testRandomPick("ab[123]c", new String[]{"ab1c", "ab2c", "ab3c"}); + testRandomPick("1234.[abcd]", new String[]{"1234.a", "1234.b", "1234.c", "1234.d"}); + } + + /** + * Test a pattern with wildcards which should produce several different results randomly + * @param inPattern pattern for site name + * @param inExpected array of expected resolved names + */ + private void testRandomPick(String inPattern, String[] inExpected) + { + HashSet results = new HashSet(); + for (int i=0; i<30; i++) + { + results.add(SiteNameUtils.pickServerUrl(inPattern)); + } + // Check that all expected results were returned + assertEquals(inExpected.length, results.size()); + for (String expec : inExpected) { + assertTrue(results.contains(expec)); + } + } +} diff --git a/test/tim/prune/jpeg/drew/RationalTest.java b/test/tim/prune/jpeg/drew/RationalTest.java new file mode 100644 index 0000000..ae97e0f --- /dev/null +++ b/test/tim/prune/jpeg/drew/RationalTest.java @@ -0,0 +1,71 @@ +package tim.prune.jpeg.drew; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * JUnit tests for the Rational values used by the Exif + */ +class RationalTest +{ + @Test + void testManyInts() + { + testIntVal(0, 0, 0); + testIntVal(1, 0, 0); + testIntVal(0, 1, 0); + for (int i=0; i<16000; i++) + { + testIntVal(0, i, 0); + testIntVal(i, 0, 0); + testIntVal(i, 1, i); + testIntVal(-i, 1, -i); + testIntVal(i*2, 2, i); + testIntVal(i*2+1, 2, i); // rounding down the 0.5 + testIntVal(-i*2, 2, -i); + testIntVal(i*2, -2, -i); + testIntVal(-i*2, -2, i); + } + } + + /** + * Check that a rational converts to an integer properly + * @param inTop number on top of the rational (numerator) + * @param inBottom number on bottom of the rational (denominator) + * @param inExpected expected int value + */ + private void testIntVal(long inTop, long inBottom, int inExpected) + { + Rational value = new Rational(inTop, inBottom); + assertEquals(inExpected, value.intValue(), "" + inTop + "/" + inBottom); + } + + @Test + void testManyDoubles() + { + for (int i=0; i<16000; i++) + { + testDoubleVal(0, i, 0.0); + testDoubleVal(i, 0, 0.0); + testDoubleVal(i, 1, i); + testDoubleVal(i*2, 2, i); + testDoubleVal(i*2+1, 2, i+0.5); + testDoubleVal(i*2, -2, -i); + } + + testDoubleVal(123, 3, 123.0/3.0); + } + + /** + * Check that a rational converts to a double properly + * @param inTop number on top of the rational (numerator) + * @param inBottom number on bottom of the rational (denominator) + * @param inExpected expected double value (exact) + */ + private void testDoubleVal(long inTop, long inBottom, double inExpected) + { + Rational value = new Rational(inTop, inBottom); + assertEquals(inExpected, value.doubleValue(), 0.0, "" + inTop + "/" + inBottom); + } +}