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

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

see #21131 - shift image in X/Y axis relative to the movement direction, not the image direction

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