1 | package nanolog;
|
---|
2 |
|
---|
3 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
4 |
|
---|
5 | import java.util.ArrayList;
|
---|
6 | import java.util.Collections;
|
---|
7 | import java.util.List;
|
---|
8 |
|
---|
9 | import javax.swing.JOptionPane;
|
---|
10 |
|
---|
11 | import org.openstreetmap.josm.Main;
|
---|
12 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
13 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
14 | import org.openstreetmap.josm.data.gpx.GpxData;
|
---|
15 | import org.openstreetmap.josm.data.gpx.GpxTrack;
|
---|
16 | import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
|
---|
17 | import org.openstreetmap.josm.data.gpx.WayPoint;
|
---|
18 | import org.openstreetmap.josm.tools.UncheckedParseException;
|
---|
19 | import org.openstreetmap.josm.tools.date.DateUtils;
|
---|
20 |
|
---|
21 | /**
|
---|
22 | * A class that establishes correlation between GPS trace and NanoLog. Mostly copied from
|
---|
23 | * {@link org.openstreetmap.josm.gui.layer.geoimage.CorrelateGpxWithImages}, thus licensed GPL.
|
---|
24 | *
|
---|
25 | * @author zverik
|
---|
26 | */
|
---|
27 | public final class Correlator {
|
---|
28 |
|
---|
29 | private Correlator() {
|
---|
30 | // Hide default constructor for utilities classes
|
---|
31 | }
|
---|
32 |
|
---|
33 | /**
|
---|
34 | * Matches entries to GPX so most points are on the trace.
|
---|
35 | */
|
---|
36 | public static long crudeMatch(List<NanoLogEntry> entries, GpxData data) {
|
---|
37 | List<NanoLogEntry> sortedEntries = new ArrayList<>(entries);
|
---|
38 | Collections.sort(sortedEntries);
|
---|
39 | long firstExifDate = sortedEntries.get(0).getTime().getTime();
|
---|
40 | long firstGPXDate = -1;
|
---|
41 | outer:
|
---|
42 | for (GpxTrack trk : data.tracks) {
|
---|
43 | for (GpxTrackSegment segment : trk.getSegments()) {
|
---|
44 | for (WayPoint curWp : segment.getWayPoints()) {
|
---|
45 | String curDateWpStr = (String) curWp.attr.get("time");
|
---|
46 | if (curDateWpStr == null) {
|
---|
47 | continue;
|
---|
48 | }
|
---|
49 |
|
---|
50 | try {
|
---|
51 | firstGPXDate = DateUtils.fromString(curDateWpStr).getTime();
|
---|
52 | break outer;
|
---|
53 | } catch (Exception e) {
|
---|
54 | Main.warn(e);
|
---|
55 | }
|
---|
56 | }
|
---|
57 | }
|
---|
58 | }
|
---|
59 |
|
---|
60 | // No GPX timestamps found, exit
|
---|
61 | if (firstGPXDate < 0) {
|
---|
62 | JOptionPane.showMessageDialog(Main.parent,
|
---|
63 | tr("The selected GPX track does not contain timestamps. Please select another one."),
|
---|
64 | tr("GPX Track has no time information"), JOptionPane.WARNING_MESSAGE);
|
---|
65 | return 0;
|
---|
66 | }
|
---|
67 |
|
---|
68 | return firstExifDate - firstGPXDate;
|
---|
69 | }
|
---|
70 |
|
---|
71 | public static void revertPos(List<NanoLogEntry> entries) {
|
---|
72 | for (NanoLogEntry entry : entries) {
|
---|
73 | entry.setPos(entry.getBasePos());
|
---|
74 | }
|
---|
75 | }
|
---|
76 |
|
---|
77 | /**
|
---|
78 | * Offset is in 1/1000 of a second.
|
---|
79 | */
|
---|
80 | public static void correlate(List<NanoLogEntry> entries, GpxData data, long offset) {
|
---|
81 | List<NanoLogEntry> sortedEntries = new ArrayList<>(entries);
|
---|
82 | //int ret = 0;
|
---|
83 | Collections.sort(sortedEntries);
|
---|
84 | for (GpxTrack track : data.tracks) {
|
---|
85 | for (GpxTrackSegment segment : track.getSegments()) {
|
---|
86 | long prevWpTime = 0;
|
---|
87 | WayPoint prevWp = null;
|
---|
88 |
|
---|
89 | for (WayPoint curWp : segment.getWayPoints()) {
|
---|
90 |
|
---|
91 | String curWpTimeStr = (String) curWp.attr.get("time");
|
---|
92 | if (curWpTimeStr != null) {
|
---|
93 | try {
|
---|
94 | long curWpTime = DateUtils.fromString(curWpTimeStr).getTime() + offset;
|
---|
95 | /*ret +=*/ matchPoints(sortedEntries, prevWp, prevWpTime, curWp, curWpTime, offset);
|
---|
96 |
|
---|
97 | prevWp = curWp;
|
---|
98 | prevWpTime = curWpTime;
|
---|
99 |
|
---|
100 | } catch (UncheckedParseException e) {
|
---|
101 | Main.error("Error while parsing date \"" + curWpTimeStr + '"');
|
---|
102 | Main.error(e);
|
---|
103 | prevWp = null;
|
---|
104 | prevWpTime = 0;
|
---|
105 | }
|
---|
106 | } else {
|
---|
107 | prevWp = null;
|
---|
108 | prevWpTime = 0;
|
---|
109 | }
|
---|
110 | }
|
---|
111 | }
|
---|
112 | }
|
---|
113 | }
|
---|
114 |
|
---|
115 | private static int matchPoints(List<NanoLogEntry> entries, WayPoint prevWp, long prevWpTime,
|
---|
116 | WayPoint curWp, long curWpTime, long offset) {
|
---|
117 | // Time between the track point and the previous one, 5 sec if first point, i.e. photos take
|
---|
118 | // 5 sec before the first track point can be assumed to be take at the starting position
|
---|
119 | long interval = prevWpTime > 0 ? Math.abs(curWpTime - prevWpTime) : 5 * 1000;
|
---|
120 | int ret = 0;
|
---|
121 |
|
---|
122 | // i is the index of the timewise last photo that has the same or earlier EXIF time
|
---|
123 | int i = getLastIndexOfListBefore(entries, curWpTime);
|
---|
124 |
|
---|
125 | // no photos match
|
---|
126 | if (i < 0)
|
---|
127 | return 0;
|
---|
128 |
|
---|
129 | Integer direction = null;
|
---|
130 | if (prevWp != null) {
|
---|
131 | direction = Long.valueOf(Math.round(180.0 / Math.PI * prevWp.getCoor().heading(curWp.getCoor()))).intValue();
|
---|
132 | }
|
---|
133 |
|
---|
134 | // First trackpoint, then interval is set to five seconds, i.e. photos up to five seconds
|
---|
135 | // before the first point will be geotagged with the starting point
|
---|
136 | if (prevWpTime == 0 || curWpTime <= prevWpTime) {
|
---|
137 | while (true) {
|
---|
138 | if (i < 0) {
|
---|
139 | break;
|
---|
140 | }
|
---|
141 | final NanoLogEntry curImg = entries.get(i);
|
---|
142 | long time = curImg.getTime().getTime();
|
---|
143 | if (time > curWpTime || time < curWpTime - interval) {
|
---|
144 | break;
|
---|
145 | }
|
---|
146 | if (curImg.getPos() == null) {
|
---|
147 | curImg.setPos(curWp.getCoor());
|
---|
148 | curImg.setDirection(direction);
|
---|
149 | ret++;
|
---|
150 | }
|
---|
151 | i--;
|
---|
152 | }
|
---|
153 | return ret;
|
---|
154 | }
|
---|
155 |
|
---|
156 | // This code gives a simple linear interpolation of the coordinates between current and
|
---|
157 | // previous track point assuming a constant speed in between
|
---|
158 | while (true) {
|
---|
159 | if (i < 0) {
|
---|
160 | break;
|
---|
161 | }
|
---|
162 | NanoLogEntry curImg = entries.get(i);
|
---|
163 | long imgTime = curImg.getTime().getTime();
|
---|
164 | if (imgTime < prevWpTime) {
|
---|
165 | break;
|
---|
166 | }
|
---|
167 |
|
---|
168 | if (curImg.getPos() == null && prevWp != null) {
|
---|
169 | // The values of timeDiff are between 0 and 1, it is not seconds but a dimensionless variable
|
---|
170 | double timeDiff = (double) (imgTime - prevWpTime) / interval;
|
---|
171 | curImg.setPos(prevWp.getCoor().interpolate(curWp.getCoor(), timeDiff));
|
---|
172 | curImg.setDirection(direction);
|
---|
173 |
|
---|
174 | ret++;
|
---|
175 | }
|
---|
176 | i--;
|
---|
177 | }
|
---|
178 | return ret;
|
---|
179 | }
|
---|
180 |
|
---|
181 | private static int getLastIndexOfListBefore(List<NanoLogEntry> entries, long searchedTime) {
|
---|
182 | int lstSize = entries.size();
|
---|
183 |
|
---|
184 | // No photos or the first photo taken is later than the search period
|
---|
185 | if (lstSize == 0 || searchedTime < entries.get(0).getTime().getTime())
|
---|
186 | return -1;
|
---|
187 |
|
---|
188 | // The search period is later than the last photo
|
---|
189 | if (searchedTime > entries.get(lstSize - 1).getTime().getTime())
|
---|
190 | return lstSize-1;
|
---|
191 |
|
---|
192 | // The searched index is somewhere in the middle, do a binary search from the beginning
|
---|
193 | int curIndex = 0;
|
---|
194 | int startIndex = 0;
|
---|
195 | int endIndex = lstSize-1;
|
---|
196 | while (endIndex - startIndex > 1) {
|
---|
197 | curIndex = (endIndex + startIndex) / 2;
|
---|
198 | if (searchedTime > entries.get(curIndex).getTime().getTime()) {
|
---|
199 | startIndex = curIndex;
|
---|
200 | } else {
|
---|
201 | endIndex = curIndex;
|
---|
202 | }
|
---|
203 | }
|
---|
204 | if (searchedTime < entries.get(endIndex).getTime().getTime())
|
---|
205 | return startIndex;
|
---|
206 |
|
---|
207 | // This final loop is to check if photos with the exact same EXIF time follows
|
---|
208 | while ((endIndex < (lstSize-1)) && (entries.get(endIndex).getTime().getTime()
|
---|
209 | == entries.get(endIndex + 1).getTime().getTime())) {
|
---|
210 | endIndex++;
|
---|
211 | }
|
---|
212 | return endIndex;
|
---|
213 | }
|
---|
214 |
|
---|
215 | /**
|
---|
216 | * Returns date of a potential point on GPX track (which can be between points).
|
---|
217 | */
|
---|
218 | public static long getGpxDate(GpxData data, LatLon pos) {
|
---|
219 | EastNorth en = Main.getProjection().latlon2eastNorth(pos);
|
---|
220 | for (GpxTrack track : data.tracks) {
|
---|
221 | for (GpxTrackSegment segment : track.getSegments()) {
|
---|
222 | long prevWpTime = 0;
|
---|
223 | WayPoint prevWp = null;
|
---|
224 | for (WayPoint curWp : segment.getWayPoints()) {
|
---|
225 | String curWpTimeStr = (String) curWp.attr.get("time");
|
---|
226 | if (curWpTimeStr != null) {
|
---|
227 | try {
|
---|
228 | long curWpTime = DateUtils.fromString(curWpTimeStr).getTime();
|
---|
229 | if (prevWp != null) {
|
---|
230 | EastNorth c1 = Main.getProjection().latlon2eastNorth(prevWp.getCoor());
|
---|
231 | EastNorth c2 = Main.getProjection().latlon2eastNorth(curWp.getCoor());
|
---|
232 | if (!c1.equals(c2)) {
|
---|
233 | EastNorth middle = getSegmentAltitudeIntersection(c1, c2, en);
|
---|
234 | if (middle != null && en.distance(middle) < 1) {
|
---|
235 | // found our point, no further search is neccessary
|
---|
236 | double prop = c1.east() == c2.east()
|
---|
237 | ? (middle.north() - c1.north()) / (c2.north() - c1.north())
|
---|
238 | : (middle.east() - c1.east()) / (c2.east() - c1.east());
|
---|
239 | if (prop >= 0 && prop <= 1) {
|
---|
240 | return Math.round(prevWpTime + prop * (curWpTime - prevWpTime));
|
---|
241 | }
|
---|
242 | }
|
---|
243 | }
|
---|
244 | }
|
---|
245 |
|
---|
246 | prevWp = curWp;
|
---|
247 | prevWpTime = curWpTime;
|
---|
248 | } catch (UncheckedParseException e) {
|
---|
249 | Main.error("Error while parsing date \"" + curWpTimeStr + '"');
|
---|
250 | Main.error(e);
|
---|
251 | prevWp = null;
|
---|
252 | prevWpTime = 0;
|
---|
253 | }
|
---|
254 | } else {
|
---|
255 | prevWp = null;
|
---|
256 | prevWpTime = 0;
|
---|
257 | }
|
---|
258 | }
|
---|
259 | }
|
---|
260 | }
|
---|
261 | return 0;
|
---|
262 | }
|
---|
263 |
|
---|
264 | /**
|
---|
265 | * Returns the coordinate of intersection of segment p1-p2 and an altitude
|
---|
266 | * to it starting at point p. If the line defined with p1-p2 intersects
|
---|
267 | * its altitude out of p1-p2, null is returned.
|
---|
268 | * @return Intersection coordinate or null
|
---|
269 | **/
|
---|
270 | public static EastNorth getSegmentAltitudeIntersection(EastNorth p1, EastNorth p2, EastNorth point) {
|
---|
271 | double ldx = p2.getX() - p1.getX();
|
---|
272 | double ldy = p2.getY() - p1.getY();
|
---|
273 |
|
---|
274 | if (ldx == 0 && ldy == 0) //segment zero length
|
---|
275 | return p1;
|
---|
276 |
|
---|
277 | double pdx = point.getX() - p1.getX();
|
---|
278 | double pdy = point.getY() - p1.getY();
|
---|
279 |
|
---|
280 | double offset = (pdx * ldx + pdy * ldy) / (ldx * ldx + ldy * ldy);
|
---|
281 |
|
---|
282 | if (offset < -1e-8 || offset > 1+1e-8) return null;
|
---|
283 | if (offset < 1e-8)
|
---|
284 | return p1;
|
---|
285 | else if (offset > 1-1e-8)
|
---|
286 | return p2;
|
---|
287 | else
|
---|
288 | return new EastNorth(p1.getX() + ldx * offset, p1.getY() + ldy * offset);
|
---|
289 | }
|
---|
290 |
|
---|
291 | }
|
---|