1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.data.gpx;
|
---|
3 |
|
---|
4 | import java.util.ArrayList;
|
---|
5 | import java.util.Date;
|
---|
6 | import java.util.List;
|
---|
7 | import java.util.concurrent.TimeUnit;
|
---|
8 |
|
---|
9 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
10 | import org.openstreetmap.josm.tools.Logging;
|
---|
11 | import org.openstreetmap.josm.tools.Pair;
|
---|
12 |
|
---|
13 | /**
|
---|
14 | * Correlation logic for {@code CorrelateGpxWithImages}.
|
---|
15 | * @since 14205
|
---|
16 | */
|
---|
17 | public 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 | }
|
---|