/* Copyright (c) 2010, skobbler GmbH * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of the project nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package org.openstreetmap.josm.plugins.mapdust; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.List; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; import org.openstreetmap.josm.gui.JosmUserIdentityManager; import org.openstreetmap.josm.gui.MapFrame; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.NavigatableComponent; import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; import org.openstreetmap.josm.plugins.Plugin; import org.openstreetmap.josm.plugins.PluginInformation; import org.openstreetmap.josm.plugins.mapdust.gui.MapdustGUI; import org.openstreetmap.josm.plugins.mapdust.gui.component.dialog.CreateBugDialog; import org.openstreetmap.josm.plugins.mapdust.gui.observer.MapdustBugObserver; import org.openstreetmap.josm.plugins.mapdust.gui.observer.MapdustUpdateObserver; import org.openstreetmap.josm.plugins.mapdust.gui.value.MapdustPluginState; import org.openstreetmap.josm.plugins.mapdust.service.MapdustServiceHandler; import org.openstreetmap.josm.plugins.mapdust.service.MapdustServiceHandlerException; import org.openstreetmap.josm.plugins.mapdust.service.value.BoundingBox; import org.openstreetmap.josm.plugins.mapdust.service.value.MapdustBug; import org.openstreetmap.josm.plugins.mapdust.service.value.MapdustBugFilter; import org.openstreetmap.josm.plugins.mapdust.service.value.MapdustRelevance; import org.openstreetmap.josm.tools.Shortcut; /** * This is the main class of the MapDust plug-in. Defines the MapDust plug-in * main functionality. * * @author Bea */ public class MapdustPlugin extends Plugin implements LayerChangeListener, ZoomChangeListener, PreferenceChangedListener, MouseListener, MapdustUpdateObserver, MapdustBugObserver { /** The graphical user interface of the plug-in */ private MapdustGUI mapdustGUI; /** The layer of the MapDust plug-in */ private MapdustLayer mapdustLayer; /** The <code>CreateIssueDialog</code> object */ private CreateBugDialog dialog; /** The list of <code>MapdustBug</code> objects */ private List<MapdustBug> mapdustBugList; /** The bounding box from where the MapDust bugs are down-loaded */ private BoundingBox bBox; /** The shortcut to access MapDust GUI */ private Shortcut shortcut; /** * The <code>MapdustBugFilter</code> object representing the selected * filters */ private MapdustBugFilter filter; /** Specifies if there was or not an error down-loading the data */ protected boolean wasError = false; /** * Builds a new <code>MapDustPlugin</code> object based on the given * arguments. * * @param info The <code>MapDustPlugin</code> object */ public MapdustPlugin(PluginInformation info) { super(info); this.filter = null; this.bBox = null; initializePlugin(); } /** * Initialize the <code>MapdustPlugin</code> object. Creates the * <code>MapdustGUI</code> and initializes the following variables with * default values: 'mapdust.pluginState', 'mapdust.nickname', * 'mapdust.showError', 'mapdust.version' and 'mapdust.localVersion'. */ private void initializePlugin() { /* create MapDust Shortcut */ this.shortcut = Shortcut.registerShortcut("MapDust", tr("Toggle: {0}", tr("Open MapDust")), KeyEvent.VK_0, Shortcut.ALT_SHIFT); /* add default values for static variables */ Main.pref.put("mapdust.pluginState", MapdustPluginState.ONLINE.getValue()); Main.pref.put("mapdust.nickname", ""); Main.pref.put("mapdust.showError", true); Main.pref.put("mapdust.version", getPluginInformation().version); Main.pref.put("mapdust.localVersion", getPluginInformation().localversion); Main.pref.addPreferenceChangeListener(this); } /** * Initializes the new <code>MapFrame</code>. Adds the * <code>MapdustGUI</code> to the new <code>MapFrame</code> and sets the * observers/listeners. * * @param oldMapFrame The old <code>MapFrame</code> object * @param newMapFrame The new <code>MapFrame</code> object */ @Override public void mapFrameInitialized(MapFrame oldMapFrame, MapFrame newMapFrame) { if (newMapFrame != null) { /* add MapDust dialog window */ mapdustGUI = new MapdustGUI(tr("MapDust bug reports"), "mapdust_icon.png", tr("Activates the MapDust bug reporter plugin"), shortcut, 150, this); /* add MapdustGUI */ mapdustGUI.setBounds(newMapFrame.getBounds()); mapdustGUI.addObserver(this); newMapFrame.addToggleDialog(mapdustGUI); /* add Listeners */ NavigatableComponent.addZoomChangeListener(this); Main.getLayerManager().addLayerChangeListener(this); newMapFrame.mapView.addMouseListener(this); /* put username to preferences */ Main.pref.put("mapdust.josmUserName", JosmUserIdentityManager.getInstance().getUserName()); } else { /* if new MapFrame is null, remove listener */ oldMapFrame.mapView.removeMouseListener(this); Main.getLayerManager().removeLayerChangeListener(this); NavigatableComponent.removeZoomChangeListener(this); mapdustGUI.removeObserver(this); mapdustGUI = null; } } /** * Listens for the events of type <code>PreferenceChangeEvent</code> . If * the event key is 'osm-server.username' then if the user name was changed * in Preferences and it was used as 'nickname'( the user did not changed * this completed nickname to something else ) for submitting changes to * MapDust , re-set the 'mapdust.josmUserName' and 'mapdust.nickname' * properties. * * @param event The <code>PreferenceChangeEvent</code> object */ @Override public void preferenceChanged(PreferenceChangeEvent event) { if (mapdustGUI != null && mapdustGUI.isShowing() && !wasError && mapdustLayer != null && mapdustLayer.isVisible()) { if (event.getKey().equals("osm-server.username")) { String newUserName = JosmUserIdentityManager.getInstance().getUserName(); String oldUserName = Main.pref.get("mapdust.josmUserName"); String nickname = Main.pref.get("mapdust.nickname"); if (nickname.isEmpty()) { /* nickname was not completed */ Main.pref.put("mapdust.josmUserName", newUserName); Main.pref.put("mapdust.nickname", newUserName); } else { if (nickname.equals(oldUserName)) { /* user name was used for nickname, and was not changed */ Main.pref.put("mapdust.josmUserName", newUserName); Main.pref.put("mapdust.nickname", newUserName); } else { /* user name was used for nickname, and was changed */ Main.pref.put("mapdust.josmUserName", newUserName); } } } } } /** * Updates the map and the MapDust bugs list with the given * <code>MapdustBug</code> object. If the bug is already contained in the * list and map, then this object will be updated with the new properties. * If the filter settings does not allow this new bug to be shown in the map * and list, then it will be removed from the map and list. * * @param mapdustBug The <code>MapdustBug</code> object */ @Override public synchronized void changedData(MapdustBug mapdustBug) { if (mapdustBugList == null) { mapdustBugList = new ArrayList<>(); } if (getMapdustGUI().isDialogShowing()) { if (Main.map != null && Main.map.mapView != null) { MapdustBug oldBug = null; for (MapdustBug bug : mapdustBugList) { if (bug.getId().equals(mapdustBug.getId())) { oldBug = bug; } } boolean showBug = shouldDisplay(mapdustBug); if (oldBug != null) { /* remove, add */ if (showBug) { mapdustBugList.remove(oldBug); mapdustBugList.add(0, mapdustBug); } else { mapdustBugList.remove(oldBug); } } else { /* new add */ if (showBug) { mapdustBugList.add(0, mapdustBug); } } mapdustGUI.update(mapdustBugList, this); mapdustLayer.setMapdustGUI(mapdustGUI); if (showBug) { mapdustGUI.setSelectedBug(mapdustBugList.get(0)); } else { mapdustLayer.setBugSelected(null); mapdustGUI.enableBtnPanel(true); Main.map.mapView.repaint(); String title = "MapDust"; String message = "The operation was successful."; JOptionPane.showMessageDialog(Main.parent, message, title, JOptionPane.INFORMATION_MESSAGE); } } } } /** * Verifies if the given <code>MapdustBug</code> object should be displayed * on the map and on the bugs list. A <code>MapdustBug</code> will be * displayed on the map only if it is permitted by the selected filter * settings (statuses, types, description and relevance filter). * * @param mapdustBug The <code>MapdustBug</code> object * @return true if the given bug should be displayed false otherwise */ private boolean shouldDisplay(MapdustBug mapdustBug) { boolean result = true; if (filter != null) { boolean containsStatus = false; if (filter.getStatuses() != null && !filter.getStatuses().isEmpty()) { Integer statusKey = mapdustBug.getStatus().getKey(); if (filter.getStatuses().contains(statusKey)) { containsStatus = true; } } else { containsStatus = true; } boolean containsType = false; if (filter.getTypes() != null && !filter.getTypes().isEmpty()) { String typeKey = mapdustBug.getType().getKey(); if (filter.getTypes().contains(typeKey)) { containsType = true; } } else { containsType = true; } if (filter.getDescr() != null && filter.getDescr()) { /* show only bugs with isDefaultDescription = false */ if (mapdustBug.getIsDefaultDescription()) { result = false; } else { result = containsStatus && containsType; } } else { result = containsStatus && containsType; } /* check relevance filter settings */ boolean containsMinRelevance = false; if (filter.getMinRelevance() != null) { MapdustRelevance minRel = filter.getMinRelevance(); MapdustRelevance bugRel = mapdustBug.getRelevance(); if (minRel.equals(bugRel)) { containsMinRelevance = true; } else { if (bugRel.compareTo(minRel) == 1) { containsMinRelevance = true; } } } boolean containsMaxRelevance = false; if (filter.getMaxRelevance() != null) { MapdustRelevance maxRel = filter.getMaxRelevance(); MapdustRelevance bugRel = mapdustBug.getRelevance(); if (maxRel.equals(bugRel)) { containsMaxRelevance = true; } else { if (bugRel.compareTo(maxRel) == -1) { containsMaxRelevance = true; } } result = result && (containsMinRelevance && containsMaxRelevance); } } return result; } /** * No need to implement this. */ @Override public void mouseEntered(MouseEvent event) {} /** * No need to implement this. */ @Override public void mouseExited(MouseEvent arg0) {} /** * No need to implement this. */ @Override public void mousePressed(MouseEvent event) {} /** * No need to implement this. */ @Override public void mouseReleased(MouseEvent arg0) {} /** * At mouse click the following two actions can be done: adding a new bug, * and selecting a bug from the map. A bug can be added if the plug-in is * the only active plug-in and you double click on the map. You can select * a bug from the map by clicking on it. * * @param event The <code>MouseEvent</code> object */ @Override public void mouseClicked(MouseEvent event) { if (mapdustLayer != null && mapdustLayer.isVisible()) { if (event.getButton() == MouseEvent.BUTTON1) { if (event.getClickCount() == 2 && !event.isConsumed()) { if (Main.getLayerManager().getActiveLayer() == getMapdustLayer()) { /* show add bug dialog */ MapdustBug bug = mapdustGUI.getSelectedBug(); if (bug != null) { Main.pref.put("selectedBug.status", bug.getStatus() .getValue()); } else { Main.pref.put("selectedBug.status", "create"); } /* disable MapdustButtonPanel */ mapdustGUI.getPanel().disableBtnPanel(); /* create and show dialog */ dialog = new CreateBugDialog(event.getPoint(), this); dialog.showDialog(); event.consume(); return; } } if (event.getClickCount() == 1 && !event.isConsumed()) { /* allow click on the bug icon on the map */ Point p = event.getPoint(); MapdustBug nearestBug = getNearestBug(p); if (nearestBug != null) { mapdustLayer.setBugSelected(nearestBug); /* set also in the list of bugs the element */ mapdustGUI.setSelectedBug(nearestBug); Main.map.mapView.repaint(); } return; } } } } /** * Returns the nearest <code>MapdustBug</code> object to the given point on * the map. * * @param p A <code>Point</code> object * @return A <code>MapdustBug</code> object */ private MapdustBug getNearestBug(Point p) { double snapDistance = 10; double minDistanceSq = Double.MAX_VALUE; MapdustBug nearestBug = null; for (MapdustBug bug : mapdustBugList) { Point sp = Main.map.mapView.getPoint(bug.getLatLon()); double dist = p.distanceSq(sp); if (minDistanceSq > dist && p.distance(sp) < snapDistance) { minDistanceSq = p.distanceSq(sp); nearestBug = bug; } else if (minDistanceSq == dist) { nearestBug = bug; } } return nearestBug; } @Override public void layerOrderChanged(LayerOrderChangeEvent e) { } /** * Adds the <code>MapdustLayer</code> to the JOSM editor. If the list of * <code>MapdustBug</code>s is null then down-loads the data from the * MapDust Service and updates the editor with this new data. * * @param e The new added layer event */ @Override public void layerAdded(LayerAddEvent e) {} /** * Removes the <code>MapdustLayer</code> from the JOSM editor. Also closes * the MapDust plug-in window. * * @param e The new added layer event */ @Override public void layerRemoving(LayerRemoveEvent e) { if (e.getRemovedLayer() instanceof MapdustLayer) { /* remove the layer */ Main.pref.put("mapdust.pluginState", MapdustPluginState.ONLINE.getValue()); NavigatableComponent.removeZoomChangeListener(this); if (mapdustGUI != null) { Main.map.remove(mapdustGUI); mapdustGUI.destroy(); } mapdustLayer = null; filter = null; mapdustBugList = null; } } /** * Listens for the zoom change event. If the zoom was changed, then it will * down-load the MapDust bugs data from the current view. The new data will * be down-loaded only if the current bounding box is different from the * previous one. */ @Override public void zoomChanged() { if (mapdustGUI != null && mapdustGUI.isShowing() && !wasError) { boolean download = true; BoundingBox curentBBox = getBBox(); if (bBox != null) { if (bBox.equals(curentBBox)) { download = false; } } bBox = curentBBox; if (download) { updatePluginData(); } } } /** * Updates the plug-in with a new MapDust bugs data. If the filters are set * then the MapDust data will be filtered. If initialUpdate flag is true * then the plug-in is updated for the first time with the MapDust data. By * default the first time there is no filter applied to the MapDust data. * * @param filter The <code>MapdustBugFilter</code> containing the filter * settings * @param initialUpdate If true then there will be no filter applied. */ @Override public void update(MapdustBugFilter filter, boolean initialUpdate) { bBox = getBBox(); if (initialUpdate) { updatePluginData(); } else { if (filter != null) { this.filter = filter; } if (mapdustGUI != null && mapdustGUI.isShowing() && !wasError) { updatePluginData(); } } } /** * Returns the current bounding box. If the bounding box values are not in * the limits, then it will normalized. * * @return A <code>BoundingBox</code> */ private BoundingBox getBBox() { MapView mapView = Main.map.mapView; Bounds bounds = new Bounds(mapView.getLatLon(0, mapView.getHeight()), mapView.getLatLon(mapView.getWidth(), 0)); return new BoundingBox(bounds.getMin().lon(), bounds.getMin().lat(), bounds.getMax().lon(), bounds.getMax().lat()); } /** * Updates the <code>MapdustPlugin</code> data. Down-loads the * <code>MapdustBug</code> objects from the current view, and updates the * <code>MapdustGUI</code> and the map with the new data. */ private void updatePluginData() { Main.worker.execute(new Runnable() { @Override public void run() { updateMapdustData(); } }); } /** * Updates the MapDust plug-in data. Down-loads the list of * <code>MapdustBug</code> objects for the given area, and updates the map * and the MapDust layer with the new data. */ protected synchronized void updateMapdustData() { if (Main.map != null && Main.map.mapView != null) { /* Down-loads the MapDust data */ try { MapdustServiceHandler handler = new MapdustServiceHandler(); mapdustBugList = handler.getBugs(bBox, filter); wasError = false; } catch (MapdustServiceHandlerException e) { wasError = true; mapdustBugList = new ArrayList<>(); } /* update the view */ SwingUtilities.invokeLater(new Runnable() { @Override public void run() { synchronized (this) { updateView(); if (wasError) { handleError(); } } } }); } } /** * Updates the current view ( map and MapDust bug list), with the given list * of <code>MapdustBug</code> objects. */ protected void updateView() { if (Main.map != null && Main.map.mapView != null) { /* update the MapdustLayer */ boolean needRepaint = false; if (!containsMapdustLayer()) { /* first start or layer was deleted */ if (mapdustGUI.isDownloaded()) { mapdustGUI.update(mapdustBugList, this); /* create and add the layer */ mapdustLayer = new MapdustLayer("MapDust", mapdustGUI, mapdustBugList); Main.getLayerManager().addLayer(this.mapdustLayer); Main.map.mapView.moveLayer(this.mapdustLayer, 0); Main.map.mapView.addMouseListener(this); NavigatableComponent.addZoomChangeListener(this); needRepaint = true; } } else { if (mapdustLayer != null) { /* MapDust data was changed */ mapdustGUI.update(mapdustBugList, this); mapdustLayer.destroy(); mapdustLayer.update(mapdustGUI, mapdustBugList); needRepaint = true; } } if (needRepaint) { /* force repaint */ mapdustGUI.revalidate(); Main.map.mapView.revalidate(); Main.map.repaint(); } } } /** * Verifies if the <code>MapView</code> contains or not the * <code>MapdustLayer</code> layer. * * @return true if the <code>MapView</code> contains the * <code>MapdustLayer</code> false otherwise */ private boolean containsMapdustLayer() { return mapdustLayer != null && Main.getLayerManager().containsLayer(mapdustLayer); } /** * Handles the <code>MapdustServiceHandlerException</code> error. */ protected void handleError() { String showMessage = Main.pref.get("mapdust.showError"); Boolean showErrorMessage = Boolean.parseBoolean(showMessage); if (showErrorMessage) { /* show errprMessage, and remove the layer */ Main.pref.put("mapdust.showError", false); String errorMessage = "There was a Mapdust service error."; errorMessage += " Please try later."; JOptionPane.showMessageDialog(Main.parent, tr(errorMessage)); } } /** * Returns the <code>MapdustGUI</code> object * * @return the mapdustGUI */ public MapdustGUI getMapdustGUI() { return mapdustGUI; } /** * Sets the <code>MapdustGUI</code> object. * * @param mapdustGUI the mapdustGUI to set */ public void setMapdustGUI(MapdustGUI mapdustGUI) { this.mapdustGUI = mapdustGUI; } /** * Returns the <code>MapdustLayer</code> object. * * @return the mapdustLayer */ public MapdustLayer getMapdustLayer() { return mapdustLayer; } /** * Sets the <code>MapdustLayer</code> object. * * @param mapdustLayer the mapdustLayer to set */ public void setMapdustLayer(MapdustLayer mapdustLayer) { this.mapdustLayer = mapdustLayer; } /** * Returns the list of <code>MapdustBug</code> objects * * @return the mapdustBugList */ public List<MapdustBug> getMapdustBugList() { return mapdustBugList; } /** * Returns the MapDust bug filter * * @return the filter */ public MapdustBugFilter getFilter() { return filter; } }