1 | // License: WTFPL. For details, see LICENSE file.
|
---|
2 | package geochat;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
5 |
|
---|
6 | import java.io.IOException;
|
---|
7 | import java.io.UnsupportedEncodingException;
|
---|
8 | import java.net.URLEncoder;
|
---|
9 | import java.util.ArrayList;
|
---|
10 | import java.util.Date;
|
---|
11 | import java.util.HashMap;
|
---|
12 | import java.util.HashSet;
|
---|
13 | import java.util.List;
|
---|
14 | import java.util.Map;
|
---|
15 | import java.util.Set;
|
---|
16 |
|
---|
17 | import javax.json.JsonArray;
|
---|
18 | import javax.json.JsonException;
|
---|
19 | import javax.json.JsonObject;
|
---|
20 |
|
---|
21 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
22 | import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
|
---|
23 | import org.openstreetmap.josm.data.projection.Projection;
|
---|
24 | import org.openstreetmap.josm.data.projection.ProjectionRegistry;
|
---|
25 | import org.openstreetmap.josm.gui.MainApplication;
|
---|
26 | import org.openstreetmap.josm.gui.MapView;
|
---|
27 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
28 | import org.openstreetmap.josm.tools.Logging;
|
---|
29 |
|
---|
30 | /**
|
---|
31 | * This class holds all the chat data and periodically polls the server.
|
---|
32 | *
|
---|
33 | * @author zverik
|
---|
34 | */
|
---|
35 | final class ChatServerConnection {
|
---|
36 | public static final String TOKEN_PREFIX = "=";
|
---|
37 | private static final String TOKEN_PATTERN = "^[a-zA-Z0-9]{10}$";
|
---|
38 |
|
---|
39 | private int userId;
|
---|
40 | private String userName;
|
---|
41 | private static ChatServerConnection instance;
|
---|
42 | private Set<ChatServerConnectionListener> listeners;
|
---|
43 | private LogRequest requestThread;
|
---|
44 |
|
---|
45 | private ChatServerConnection() {
|
---|
46 | userId = 0;
|
---|
47 | userName = null;
|
---|
48 | listeners = new HashSet<>();
|
---|
49 | requestThread = new LogRequest();
|
---|
50 | new Thread(requestThread).start();
|
---|
51 | }
|
---|
52 |
|
---|
53 | public static ChatServerConnection getInstance() {
|
---|
54 | if (instance == null)
|
---|
55 | instance = new ChatServerConnection();
|
---|
56 | return instance;
|
---|
57 | }
|
---|
58 |
|
---|
59 | public void addListener(ChatServerConnectionListener listener) {
|
---|
60 | listeners.add(listener);
|
---|
61 | }
|
---|
62 |
|
---|
63 | public void removeListener(ChatServerConnectionListener listener) {
|
---|
64 | listeners.remove(listener);
|
---|
65 | }
|
---|
66 |
|
---|
67 | public boolean isActive() {
|
---|
68 | return isLoggedIn() && getPosition() != null;
|
---|
69 | }
|
---|
70 |
|
---|
71 | public boolean isLoggedIn() {
|
---|
72 | return userId > 0;
|
---|
73 | }
|
---|
74 |
|
---|
75 | public String getUserName() {
|
---|
76 | return userName;
|
---|
77 | }
|
---|
78 |
|
---|
79 | /**
|
---|
80 | * Test that userId is still active, log out otherwise.
|
---|
81 | */
|
---|
82 | public void checkLogin() {
|
---|
83 | autoLogin(null);
|
---|
84 | }
|
---|
85 |
|
---|
86 | /**
|
---|
87 | * Test that userId is still active, if not, tries to login with given user name.
|
---|
88 | * Does not autologin, if userName is null, obviously.
|
---|
89 | */
|
---|
90 | public void autoLogin(final String userName) {
|
---|
91 | final int uid = Config.getPref().getInt("geochat.lastuid", 0);
|
---|
92 | if (uid <= 0) {
|
---|
93 | if (userName != null && userName.length() > 1)
|
---|
94 | login(userName);
|
---|
95 | } else {
|
---|
96 | String query = "whoami&uid=" + uid;
|
---|
97 | JsonQueryUtil.queryAsync(query, new JsonQueryCallback() {
|
---|
98 | @Override
|
---|
99 | public void processJson(JsonObject json) {
|
---|
100 | if (json != null && json.get("name") != null)
|
---|
101 | login(uid, json.getString("name"));
|
---|
102 | else if (userName != null && userName.length() > 1)
|
---|
103 | login(userName);
|
---|
104 | }
|
---|
105 | });
|
---|
106 | }
|
---|
107 | }
|
---|
108 |
|
---|
109 | /**
|
---|
110 | * Waits until {@link #getPosition()} is not null, then calls {@link #autoLogin(java.lang.String)}.
|
---|
111 | * If two seconds have passed, stops the waiting. Doesn't wait if userName is empty.
|
---|
112 | */
|
---|
113 | public void autoLoginWithDelay(final String userName) {
|
---|
114 | if (userName == null || userName.length() == 0) {
|
---|
115 | checkLogin();
|
---|
116 | return;
|
---|
117 | }
|
---|
118 | new Thread(() -> {
|
---|
119 | try {
|
---|
120 | int cnt = 10;
|
---|
121 | while (getPosition() == null && cnt-- > 0) {
|
---|
122 | Thread.sleep(200);
|
---|
123 | }
|
---|
124 | } catch (InterruptedException e) {
|
---|
125 | Logging.warn(e);
|
---|
126 | }
|
---|
127 | autoLogin(userName);
|
---|
128 | }).start();
|
---|
129 | }
|
---|
130 |
|
---|
131 | public void login(final String userName) {
|
---|
132 | if (userName == null)
|
---|
133 | throw new IllegalArgumentException("userName is null");
|
---|
134 | LatLon pos = getPosition();
|
---|
135 | if (pos == null) {
|
---|
136 | fireLoginFailed("Zoom level is too low");
|
---|
137 | return;
|
---|
138 | }
|
---|
139 | String token = userName.startsWith(TOKEN_PREFIX) ? userName.substring(TOKEN_PREFIX.length()) : null;
|
---|
140 | if (token != null && !token.matches(TOKEN_PATTERN)) {
|
---|
141 | fireLoginFailed("Incorrect token format");
|
---|
142 | return;
|
---|
143 | }
|
---|
144 |
|
---|
145 | try {
|
---|
146 | String nameAttr = token != null ? "&token=" + token : "&name=" + URLEncoder.encode(userName, "UTF-8");
|
---|
147 | String query = "register&lat=" + DecimalDegreesCoordinateFormat.INSTANCE.latToString(pos)
|
---|
148 | + "&lon=" + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(pos)
|
---|
149 | + nameAttr;
|
---|
150 | JsonQueryUtil.queryAsync(query, new JsonQueryCallback() {
|
---|
151 | @Override
|
---|
152 | public void processJson(JsonObject json) {
|
---|
153 | if (json == null)
|
---|
154 | fireLoginFailed(tr("Could not get server response, check logs"));
|
---|
155 | else if (json.get("error") != null)
|
---|
156 | fireLoginFailed(tr("Failed to login as {0}:", userName) + "\n" + json.getString("error"));
|
---|
157 | else if (json.get("uid") == null)
|
---|
158 | fireLoginFailed(tr("The server did not return user ID"));
|
---|
159 | else {
|
---|
160 | String name = json.get("name") != null ? json.getString("name") : userName;
|
---|
161 | login(json.getInt("uid"), name);
|
---|
162 | }
|
---|
163 | }
|
---|
164 | });
|
---|
165 | } catch (UnsupportedEncodingException e) {
|
---|
166 | Logging.error(e);
|
---|
167 | }
|
---|
168 | }
|
---|
169 |
|
---|
170 | private void login(int userId, String userName) {
|
---|
171 | this.userId = userId;
|
---|
172 | this.userName = userName;
|
---|
173 | Config.getPref().putInt("geochat.lastuid", userId);
|
---|
174 | for (ChatServerConnectionListener listener : listeners) {
|
---|
175 | listener.loggedIn(userName);
|
---|
176 | }
|
---|
177 | }
|
---|
178 |
|
---|
179 | private void logoutIntl() {
|
---|
180 | ChatServerConnection.this.userId = 0;
|
---|
181 | ChatServerConnection.this.userName = null;
|
---|
182 | Config.getPref().put("geochat.lastuid", null);
|
---|
183 | for (ChatServerConnectionListener listener : listeners) {
|
---|
184 | listener.notLoggedIn(null);
|
---|
185 | }
|
---|
186 | }
|
---|
187 |
|
---|
188 | private void fireLoginFailed(String reason) {
|
---|
189 | for (ChatServerConnectionListener listener : listeners) {
|
---|
190 | listener.notLoggedIn(reason);
|
---|
191 | }
|
---|
192 | }
|
---|
193 |
|
---|
194 | /**
|
---|
195 | * Unregister the current user.
|
---|
196 | */
|
---|
197 | public void logout() {
|
---|
198 | if (!isLoggedIn())
|
---|
199 | return;
|
---|
200 | String query = "logout&uid=" + userId;
|
---|
201 | JsonQueryUtil.queryAsync(query, new JsonQueryCallback() {
|
---|
202 | @Override
|
---|
203 | public void processJson(JsonObject json) {
|
---|
204 | if (json != null && json.get("message") != null) {
|
---|
205 | logoutIntl();
|
---|
206 | }
|
---|
207 | }
|
---|
208 | });
|
---|
209 | }
|
---|
210 |
|
---|
211 | /**
|
---|
212 | * Unregister the current user and do not call listeners.
|
---|
213 | * Makes synchronous request to the server.
|
---|
214 | */
|
---|
215 | public void bruteLogout() throws IOException {
|
---|
216 | if (isLoggedIn())
|
---|
217 | JsonQueryUtil.query("logout&uid=" + userId);
|
---|
218 | }
|
---|
219 |
|
---|
220 | private void fireMessageFailed(String reason) {
|
---|
221 | for (ChatServerConnectionListener listener : listeners) {
|
---|
222 | listener.messageSendFailed(reason);
|
---|
223 | }
|
---|
224 | }
|
---|
225 |
|
---|
226 | /**
|
---|
227 | * Posts message to the main channel.
|
---|
228 | * @param message Message string.
|
---|
229 | * @see #postMessage(java.lang.String, java.lang.String)
|
---|
230 | */
|
---|
231 | public void postMessage(String message) {
|
---|
232 | postMessage(message, null);
|
---|
233 | }
|
---|
234 |
|
---|
235 | /**
|
---|
236 | * Posts message to the main channel or to a specific user.
|
---|
237 | * Calls listener on fail.
|
---|
238 | * @param message Message string.
|
---|
239 | * @param targetUser null if sending to everyone, name of user otherwise.
|
---|
240 | */
|
---|
241 | public void postMessage(String message, String targetUser) {
|
---|
242 | if (!isLoggedIn()) {
|
---|
243 | fireMessageFailed("Not logged in");
|
---|
244 | return;
|
---|
245 | }
|
---|
246 | LatLon pos = getPosition();
|
---|
247 | if (pos == null) {
|
---|
248 | fireMessageFailed("Zoom level is too low");
|
---|
249 | return;
|
---|
250 | }
|
---|
251 | try {
|
---|
252 | String query = "post&lat=" + DecimalDegreesCoordinateFormat.INSTANCE.latToString(pos)
|
---|
253 | + "&lon=" + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(pos)
|
---|
254 | + "&uid=" + userId
|
---|
255 | + "&message=" + URLEncoder.encode(message, "UTF8");
|
---|
256 | if (targetUser != null && targetUser.length() > 0)
|
---|
257 | query += "&to=" + URLEncoder.encode(targetUser, "UTF8");
|
---|
258 | JsonQueryUtil.queryAsync(query, new JsonQueryCallback() {
|
---|
259 | @Override
|
---|
260 | public void processJson(JsonObject json) {
|
---|
261 | if (json == null)
|
---|
262 | fireMessageFailed(tr("Could not get server response, check logs"));
|
---|
263 | else if (json.get("error") != null)
|
---|
264 | fireMessageFailed(json.getString("error"));
|
---|
265 | }
|
---|
266 | });
|
---|
267 | } catch (UnsupportedEncodingException e) {
|
---|
268 | Logging.error(e);
|
---|
269 | }
|
---|
270 | }
|
---|
271 |
|
---|
272 | /**
|
---|
273 | * Returns current coordinates or null if there is no map, or zoom is too low.
|
---|
274 | */
|
---|
275 | private static LatLon getPosition() {
|
---|
276 | if (!MainApplication.isDisplayingMapView())
|
---|
277 | return null;
|
---|
278 | if (getCurrentZoom() < 10)
|
---|
279 | return null;
|
---|
280 | Projection proj = ProjectionRegistry.getProjection();
|
---|
281 | return proj.eastNorth2latlon(MainApplication.getMap().mapView.getCenter());
|
---|
282 | }
|
---|
283 |
|
---|
284 | // Following three methods were snatched from TMSLayer
|
---|
285 | private static double latToTileY(double lat, int zoom) {
|
---|
286 | double l = lat / 180 * Math.PI;
|
---|
287 | double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
|
---|
288 | return Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI;
|
---|
289 | }
|
---|
290 |
|
---|
291 | private static double lonToTileX(double lon, int zoom) {
|
---|
292 | return Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0;
|
---|
293 | }
|
---|
294 |
|
---|
295 | public static int getCurrentZoom() {
|
---|
296 | if (!MainApplication.isDisplayingMapView()) {
|
---|
297 | return 1;
|
---|
298 | }
|
---|
299 | MapView mv = MainApplication.getMap().mapView;
|
---|
300 | LatLon topLeft = mv.getLatLon(0, 0);
|
---|
301 | LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
|
---|
302 | double x1 = lonToTileX(topLeft.lon(), 1);
|
---|
303 | double y1 = latToTileY(topLeft.lat(), 1);
|
---|
304 | double x2 = lonToTileX(botRight.lon(), 1);
|
---|
305 | double y2 = latToTileY(botRight.lat(), 1);
|
---|
306 |
|
---|
307 | int screenPixels = mv.getWidth() * mv.getHeight();
|
---|
308 | double tilePixels = Math.abs((y2 - y1) * (x2 - x1) * 256 * 256);
|
---|
309 | if (screenPixels == 0 || tilePixels == 0) {
|
---|
310 | return 1;
|
---|
311 | }
|
---|
312 | double factor = screenPixels / tilePixels;
|
---|
313 | double result = Math.log(factor) / Math.log(2) / 2 + 1;
|
---|
314 | int intResult = (int) Math.floor(result);
|
---|
315 | return intResult;
|
---|
316 | }
|
---|
317 |
|
---|
318 | private class LogRequest implements Runnable {
|
---|
319 | private static final int MAX_JUMP = 20000; // in meters
|
---|
320 | private LatLon lastPosition = null;
|
---|
321 | private long lastUserId = 0;
|
---|
322 | private long lastId = 0;
|
---|
323 | private boolean lastStatus = false;
|
---|
324 | private boolean stopping = false;
|
---|
325 |
|
---|
326 | @Override
|
---|
327 | public void run() {
|
---|
328 | // lastId = Config.getPref().getLong("geochat.lastid", 0);
|
---|
329 | int interval = Config.getPref().getInt("geochat.interval", 2);
|
---|
330 | while (!stopping) {
|
---|
331 | process();
|
---|
332 | try {
|
---|
333 | Thread.sleep(interval * 1000);
|
---|
334 | } catch (InterruptedException e) {
|
---|
335 | stopping = true;
|
---|
336 | }
|
---|
337 | }
|
---|
338 | }
|
---|
339 |
|
---|
340 | public void stop() {
|
---|
341 | stopping = true;
|
---|
342 | }
|
---|
343 |
|
---|
344 | public void process() {
|
---|
345 | if (!isLoggedIn()) {
|
---|
346 | fireStatusChanged(false);
|
---|
347 | return;
|
---|
348 | }
|
---|
349 |
|
---|
350 | LatLon pos = getPosition();
|
---|
351 | if (pos == null) {
|
---|
352 | fireStatusChanged(false);
|
---|
353 | return;
|
---|
354 | }
|
---|
355 | fireStatusChanged(true);
|
---|
356 |
|
---|
357 | final boolean needReset;
|
---|
358 | final boolean needFullReset = lastUserId != userId;
|
---|
359 | if (needFullReset || (lastPosition != null && pos.greatCircleDistance(lastPosition) > MAX_JUMP)) {
|
---|
360 | // reset messages
|
---|
361 | lastId = 0;
|
---|
362 | // Config.getPref().put("geochat.lastid", null);
|
---|
363 | needReset = true;
|
---|
364 | } else
|
---|
365 | needReset = false;
|
---|
366 | lastUserId = userId;
|
---|
367 | lastPosition = pos;
|
---|
368 |
|
---|
369 | String query = "get&lat=" + DecimalDegreesCoordinateFormat.INSTANCE.latToString(pos)
|
---|
370 | + "&lon=" + DecimalDegreesCoordinateFormat.INSTANCE.lonToString(pos)
|
---|
371 | + "&uid=" + userId + "&last=" + lastId;
|
---|
372 | JsonObject json;
|
---|
373 | try {
|
---|
374 | json = JsonQueryUtil.query(query);
|
---|
375 | } catch (IOException ex) {
|
---|
376 | json = null; // ?
|
---|
377 | }
|
---|
378 | if (json == null) {
|
---|
379 | // do nothing?
|
---|
380 | // fireLoginFailed(tr("Could not get server response, check logs"));
|
---|
381 | // logoutIntl(); // todo: uncomment?
|
---|
382 | } else if (json.get("error") != null) {
|
---|
383 | fireLoginFailed(tr("Failed to get messages as {0}:", userName) + "\n" + json.getString("error"));
|
---|
384 | logoutIntl();
|
---|
385 | } else {
|
---|
386 | if (json.get("users") != null) {
|
---|
387 | Map<String, LatLon> users = parseUsers(json.getJsonArray("users"));
|
---|
388 | for (ChatServerConnectionListener listener : listeners) {
|
---|
389 | listener.updateUsers(users);
|
---|
390 | }
|
---|
391 | }
|
---|
392 | if (json.get("messages") != null) {
|
---|
393 | List<ChatMessage> messages = parseMessages(json.getJsonArray("messages"), false);
|
---|
394 | for (ChatMessage m : messages) {
|
---|
395 | if (m.getId() > lastId)
|
---|
396 | lastId = m.getId();
|
---|
397 | }
|
---|
398 | for (ChatServerConnectionListener listener : listeners) {
|
---|
399 | listener.receivedMessages(needReset, messages);
|
---|
400 | }
|
---|
401 | }
|
---|
402 | if (json.get("private") != null) {
|
---|
403 | List<ChatMessage> messages = parseMessages(json.getJsonArray("private"), true);
|
---|
404 | for (ChatMessage m : messages) {
|
---|
405 | if (m.getId() > lastId)
|
---|
406 | lastId = m.getId();
|
---|
407 | }
|
---|
408 | for (ChatServerConnectionListener listener : listeners) {
|
---|
409 | listener.receivedPrivateMessages(needFullReset, messages);
|
---|
410 | }
|
---|
411 | }
|
---|
412 | }
|
---|
413 | // if (lastId > 0 && Config.getPref().getBoolean("geochat.store.lastid", true) )
|
---|
414 | // Config.getPref().putLong("geochat.lastid", lastId);
|
---|
415 | }
|
---|
416 |
|
---|
417 | private List<ChatMessage> parseMessages(JsonArray messages, boolean priv) {
|
---|
418 | List<ChatMessage> result = new ArrayList<>();
|
---|
419 | for (int i = 0; i < messages.size(); i++) {
|
---|
420 | try {
|
---|
421 | JsonObject msg = messages.getJsonObject(i);
|
---|
422 | long id = Long.parseLong(msg.getString("id"));
|
---|
423 | double lat = Double.parseDouble(msg.getString("lat"));
|
---|
424 | double lon = Double.parseDouble(msg.getString("lon"));
|
---|
425 | long timeStamp = Long.parseLong(msg.getString("timestamp"));
|
---|
426 | String author = msg.getString("author");
|
---|
427 | String message = msg.getString("message");
|
---|
428 | boolean incoming = msg.getBoolean("incoming");
|
---|
429 | ChatMessage cm = new ChatMessage(id, new LatLon(lat, lon), author,
|
---|
430 | incoming, message, new Date(timeStamp * 1000));
|
---|
431 | cm.setPrivate(priv);
|
---|
432 | if (msg.get("recipient") != null && !incoming)
|
---|
433 | cm.setRecipient(msg.getString("recipient"));
|
---|
434 | result.add(cm);
|
---|
435 | } catch (JsonException e) {
|
---|
436 | Logging.trace(e);
|
---|
437 | }
|
---|
438 | }
|
---|
439 | return result;
|
---|
440 | }
|
---|
441 |
|
---|
442 | private Map<String, LatLon> parseUsers(JsonArray users) {
|
---|
443 | Map<String, LatLon> result = new HashMap<>();
|
---|
444 | for (int i = 0; i < users.size(); i++) {
|
---|
445 | try {
|
---|
446 | JsonObject user = users.getJsonObject(i);
|
---|
447 | String name = user.getString("user");
|
---|
448 | double lat = Double.parseDouble(user.getString("lat"));
|
---|
449 | double lon = Double.parseDouble(user.getString("lon"));
|
---|
450 | result.put(name, new LatLon(lat, lon));
|
---|
451 | } catch (JsonException e) {
|
---|
452 | Logging.trace(e);
|
---|
453 | }
|
---|
454 | }
|
---|
455 | return result;
|
---|
456 | }
|
---|
457 |
|
---|
458 | private void fireStatusChanged(boolean newStatus) {
|
---|
459 | if (newStatus == lastStatus)
|
---|
460 | return;
|
---|
461 | lastStatus = newStatus;
|
---|
462 | for (ChatServerConnectionListener listener : listeners) {
|
---|
463 | listener.statusChanged(newStatus);
|
---|
464 | }
|
---|
465 | }
|
---|
466 | }
|
---|
467 | }
|
---|