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