source: josm/trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java@ 14456

Last change on this file since 14456 was 14456, checked in by Don-vip, 5 years ago

fix #16995 - de-duplicate storage of timestamp within WayPoint and refactor some methods, added documentation, added some robustness against legacy code (will also log a warning if detected). Patch by cmuelle8, modified

  • Property svn:eol-style set to native
File size: 13.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.gpx;
3
4import java.util.ArrayList;
5import java.util.Date;
6import java.util.List;
7import java.util.concurrent.TimeUnit;
8
9import org.openstreetmap.josm.spi.preferences.Config;
10import org.openstreetmap.josm.tools.Logging;
11import org.openstreetmap.josm.tools.Pair;
12
13/**
14 * Correlation logic for {@code CorrelateGpxWithImages}.
15 * @since 14205
16 */
17public final class GpxImageCorrelation {
18
19 private GpxImageCorrelation() {
20 // Hide public constructor
21 }
22
23 /**
24 * Match a list of photos to a gpx track with a given offset.
25 * All images need a exifTime attribute and the List must be sorted according to these times.
26 * @param images images to match
27 * @param selectedGpx selected GPX data
28 * @param offset offset
29 * @param forceTags force tagging of all photos, otherwise prefs are used
30 * @return number of matched points
31 */
32 public static int matchGpxTrack(List<? extends GpxImageEntry> images, GpxData selectedGpx, long offset, boolean forceTags) {
33 int ret = 0;
34
35 long prevWpTime = 0;
36 WayPoint prevWp = null;
37
38 List<List<List<WayPoint>>> trks = new ArrayList<>();
39
40 for (GpxTrack trk : selectedGpx.tracks) {
41 List<List<WayPoint>> segs = new ArrayList<>();
42 for (GpxTrackSegment seg : trk.getSegments()) {
43 List<WayPoint> wps = new ArrayList<>(seg.getWayPoints());
44 if (!wps.isEmpty()) {
45 //remove waypoints at the beginning of the track/segment without timestamps
46 int wp;
47 for (wp = 0; wp < wps.size(); wp++) {
48 if (wps.get(wp).hasDate()) {
49 break;
50 }
51 }
52 if (wp == 0) {
53 segs.add(wps);
54 } else if (wp < wps.size()) {
55 segs.add(wps.subList(wp, wps.size()));
56 }
57 }
58 }
59 //sort segments by first waypoint
60 if (!segs.isEmpty()) {
61 segs.sort((o1, o2) -> {
62 if (o1.isEmpty() || o2.isEmpty())
63 return 0;
64 return o1.get(0).compareTo(o2.get(0));
65 });
66 trks.add(segs);
67 }
68 }
69 //sort tracks by first waypoint of first segment
70 trks.sort((o1, o2) -> {
71 if (o1.isEmpty() || o1.get(0).isEmpty()
72 || o2.isEmpty() || o2.get(0).isEmpty())
73 return 0;
74 return o1.get(0).get(0).compareTo(o2.get(0).get(0));
75 });
76
77 boolean trkInt, trkTag, segInt, segTag;
78 int trkTime, trkDist, trkTagTime, segTime, segDist, segTagTime;
79
80 if (forceTags) { //temporary option to override advanced settings and activate all possible interpolations / tagging methods
81 trkInt = trkTag = segInt = segTag = true;
82 trkTime = trkDist = trkTagTime = segTime = segDist = segTagTime = Integer.MAX_VALUE;
83 } else {
84 // Load the settings
85 trkInt = Config.getPref().getBoolean("geoimage.trk.int", false);
86 trkTime = Config.getPref().getBoolean("geoimage.trk.int.time", false) ?
87 Config.getPref().getInt("geoimage.trk.int.time.val", 60) : Integer.MAX_VALUE;
88 trkDist = Config.getPref().getBoolean("geoimage.trk.int.dist", false) ?
89 Config.getPref().getInt("geoimage.trk.int.dist.val", 50) : Integer.MAX_VALUE;
90
91 trkTag = Config.getPref().getBoolean("geoimage.trk.tag", true);
92 trkTagTime = Config.getPref().getBoolean("geoimage.trk.tag.time", true) ?
93 Config.getPref().getInt("geoimage.trk.tag.time.val", 2) : Integer.MAX_VALUE;
94
95 segInt = Config.getPref().getBoolean("geoimage.seg.int", true);
96 segTime = Config.getPref().getBoolean("geoimage.seg.int.time", true) ?
97 Config.getPref().getInt("geoimage.seg.int.time.val", 60) : Integer.MAX_VALUE;
98 segDist = Config.getPref().getBoolean("geoimage.seg.int.dist", true) ?
99 Config.getPref().getInt("geoimage.seg.int.dist.val", 50) : Integer.MAX_VALUE;
100
101 segTag = Config.getPref().getBoolean("geoimage.seg.tag", true);
102 segTagTime = Config.getPref().getBoolean("geoimage.seg.tag.time", true) ?
103 Config.getPref().getInt("geoimage.seg.tag.time.val", 2) : Integer.MAX_VALUE;
104 }
105 boolean isFirst = true;
106
107 for (int t = 0; t < trks.size(); t++) {
108 List<List<WayPoint>> segs = trks.get(t);
109 for (int s = 0; s < segs.size(); s++) {
110 List<WayPoint> wps = segs.get(s);
111 for (int i = 0; i < wps.size(); i++) {
112 WayPoint curWp = wps.get(i);
113 // Interpolate timestamps in the segment, if one or more waypoints miss them
114 if (!curWp.hasDate()) {
115 //check if any of the following waypoints has a timestamp...
116 if (i > 0 && wps.get(i - 1).hasDate()) {
117 long prevWpTimeNoOffset = wps.get(i - 1).getTimeInMillis();
118 double totalDist = 0;
119 List<Pair<Double, WayPoint>> nextWps = new ArrayList<>();
120 for (int j = i; j < wps.size(); j++) {
121 totalDist += wps.get(j - 1).getCoor().greatCircleDistance(wps.get(j).getCoor());
122 nextWps.add(new Pair<>(totalDist, wps.get(j)));
123 if (wps.get(j).hasDate()) {
124 // ...if yes, interpolate everything in between
125 long timeDiff = wps.get(j).getTimeInMillis() - prevWpTimeNoOffset;
126 for (Pair<Double, WayPoint> pair : nextWps) {
127 pair.b.setTimeInMillis((long) (prevWpTimeNoOffset + (timeDiff * (pair.a / totalDist))));
128 }
129 break;
130 }
131 }
132 if (!curWp.hasDate()) {
133 break; //It's pointless to continue with this segment, because none of the following waypoints had a timestamp
134 }
135 } else {
136 // Timestamps on waypoints without preceding timestamps in the same segment can not be interpolated, so try next one
137 continue;
138 }
139 }
140
141 final long curWpTime = curWp.getTimeInMillis() + offset;
142 boolean interpolate = true;
143 int tagTime = 0;
144 if (i == 0) {
145 if (s == 0) { //First segment of the track, so apply settings for tracks
146 if (!trkInt || isFirst || prevWp == null ||
147 Math.abs(curWpTime - prevWpTime) > TimeUnit.MINUTES.toMillis(trkTime) ||
148 prevWp.getCoor().greatCircleDistance(curWp.getCoor()) > trkDist) {
149 isFirst = false;
150 interpolate = false;
151 if (trkTag) {
152 tagTime = trkTagTime;
153 }
154 }
155 } else { //Apply settings for segments
156 if (!segInt || prevWp == null ||
157 Math.abs(curWpTime - prevWpTime) > TimeUnit.MINUTES.toMillis(segTime) ||
158 prevWp.getCoor().greatCircleDistance(curWp.getCoor()) > segDist) {
159 interpolate = false;
160 if (segTag) {
161 tagTime = segTagTime;
162 }
163 }
164 }
165 }
166 ret += matchPoints(images, prevWp, prevWpTime, curWp, curWpTime, offset, interpolate, tagTime, false);
167 prevWp = curWp;
168 prevWpTime = curWpTime;
169 }
170 }
171 }
172 if (trkTag) {
173 ret += matchPoints(images, prevWp, prevWpTime, prevWp, prevWpTime, offset, false, trkTagTime, true);
174 }
175 return ret;
176 }
177
178 private static Double getElevation(WayPoint wp) {
179 String value = wp.getString(GpxConstants.PT_ELE);
180 if (value != null && !value.isEmpty()) {
181 try {
182 return Double.valueOf(value);
183 } catch (NumberFormatException e) {
184 Logging.warn(e);
185 }
186 }
187 return null;
188 }
189
190 private static int matchPoints(List<? extends GpxImageEntry> images, WayPoint prevWp, long prevWpTime, WayPoint curWp, long curWpTime,
191 long offset, boolean interpolate, int tagTime, boolean isLast) {
192
193 int ret = 0;
194
195 // i is the index of the timewise last photo that has the same or earlier EXIF time
196 int i;
197 if (isLast) {
198 i = images.size() - 1;
199 } else {
200 i = getLastIndexOfListBefore(images, curWpTime);
201 }
202
203 // no photos match
204 if (i < 0)
205 return 0;
206
207 Double speed = null;
208 Double prevElevation = null;
209
210 if (prevWp != null && interpolate) {
211 double distance = prevWp.getCoor().greatCircleDistance(curWp.getCoor());
212 // This is in km/h, 3.6 * m/s
213 if (curWpTime > prevWpTime) {
214 speed = 3600 * distance / (curWpTime - prevWpTime);
215 }
216 prevElevation = getElevation(prevWp);
217 }
218
219 Double curElevation = getElevation(curWp);
220
221 if (!interpolate || isLast) {
222 final long half = Math.abs(curWpTime - prevWpTime) / 2;
223 while (i >= 0) {
224 final GpxImageEntry curImg = images.get(i);
225 final GpxImageEntry curTmp = curImg.getTmp();
226 final long time = curImg.getExifTime().getTime();
227 if ((!isLast && time > curWpTime) || time < prevWpTime) {
228 break;
229 }
230 long tagms = TimeUnit.MINUTES.toMillis(tagTime);
231 if (curTmp.getPos() == null &&
232 (Math.abs(time - curWpTime) <= tagms
233 || Math.abs(prevWpTime - time) <= tagms)) {
234 if (prevWp != null && time < curWpTime - half) {
235 curTmp.setPos(prevWp.getCoor());
236 } else {
237 curTmp.setPos(curWp.getCoor());
238 }
239 curTmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
240 curTmp.flagNewGpsData();
241 ret++;
242 }
243 i--;
244 }
245 } else if (prevWp != null) {
246 // This code gives a simple linear interpolation of the coordinates between current and
247 // previous track point assuming a constant speed in between
248 while (i >= 0) {
249 GpxImageEntry curImg = images.get(i);
250 GpxImageEntry curTmp = curImg.getTmp();
251 final long imgTime = curImg.getExifTime().getTime();
252 if (imgTime < prevWpTime) {
253 break;
254 }
255 if (curTmp.getPos() == null) {
256 // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless variable
257 double timeDiff = (double) (imgTime - prevWpTime) / Math.abs(curWpTime - prevWpTime);
258 curTmp.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff));
259 curTmp.setSpeed(speed);
260 if (curElevation != null && prevElevation != null) {
261 curTmp.setElevation(prevElevation + (curElevation - prevElevation) * timeDiff);
262 }
263 curTmp.setGpsTime(new Date(curImg.getExifTime().getTime() - offset));
264 curTmp.flagNewGpsData();
265
266 ret++;
267 }
268 i--;
269 }
270 }
271 return ret;
272 }
273
274 private static int getLastIndexOfListBefore(List<? extends GpxImageEntry> images, long searchedTime) {
275 int lstSize = images.size();
276
277 // No photos or the first photo taken is later than the search period
278 if (lstSize == 0 || searchedTime < images.get(0).getExifTime().getTime())
279 return -1;
280
281 // The search period is later than the last photo
282 if (searchedTime > images.get(lstSize - 1).getExifTime().getTime())
283 return lstSize-1;
284
285 // The searched index is somewhere in the middle, do a binary search from the beginning
286 int curIndex;
287 int startIndex = 0;
288 int endIndex = lstSize-1;
289 while (endIndex - startIndex > 1) {
290 curIndex = (endIndex + startIndex) / 2;
291 if (searchedTime > images.get(curIndex).getExifTime().getTime()) {
292 startIndex = curIndex;
293 } else {
294 endIndex = curIndex;
295 }
296 }
297 if (searchedTime < images.get(endIndex).getExifTime().getTime())
298 return startIndex;
299
300 // This final loop is to check if photos with the exact same EXIF time follows
301 while ((endIndex < (lstSize - 1)) && (images.get(endIndex).getExifTime().getTime()
302 == images.get(endIndex + 1).getExifTime().getTime())) {
303 endIndex++;
304 }
305 return endIndex;
306 }
307}
Note: See TracBrowser for help on using the repository browser.