/* ------------------------------------------------------------------------- OpenTripPlanner GWT Client Copyright (C) 2015 Mecatran - info@mecatran.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ------------------------------------------------------------------------- */ package com.mecatran.otp.gwt.client.view; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Set; import org.gwtopenmaps.openlayers.client.Bounds; import org.gwtopenmaps.openlayers.client.LonLat; import org.gwtopenmaps.openlayers.client.Map; import org.gwtopenmaps.openlayers.client.MapOptions; import org.gwtopenmaps.openlayers.client.MapWidget; import org.gwtopenmaps.openlayers.client.Pixel; import org.gwtopenmaps.openlayers.client.Projection; import org.gwtopenmaps.openlayers.client.Size; import org.gwtopenmaps.openlayers.client.control.DragFeature; import org.gwtopenmaps.openlayers.client.control.DragFeature.DragFeatureListener; import org.gwtopenmaps.openlayers.client.control.DragFeatureOptions; import org.gwtopenmaps.openlayers.client.control.LayerSwitcher; import org.gwtopenmaps.openlayers.client.control.OverviewMap; import org.gwtopenmaps.openlayers.client.control.ScaleLine; import org.gwtopenmaps.openlayers.client.control.SelectFeature; import org.gwtopenmaps.openlayers.client.control.SelectFeature.ClickFeatureListener; import org.gwtopenmaps.openlayers.client.control.SelectFeatureOptions; import org.gwtopenmaps.openlayers.client.event.MapClickListener; import org.gwtopenmaps.openlayers.client.event.MapMoveListener; import org.gwtopenmaps.openlayers.client.event.MapZoomListener; import org.gwtopenmaps.openlayers.client.feature.VectorFeature; import org.gwtopenmaps.openlayers.client.layer.Layer; import org.gwtopenmaps.openlayers.client.layer.OSM; import org.gwtopenmaps.openlayers.client.layer.OSMOptions; import org.gwtopenmaps.openlayers.client.layer.Vector; import org.gwtopenmaps.openlayers.client.layer.VectorOptions; import org.gwtopenmaps.openlayers.client.layer.XYZ; import org.gwtopenmaps.openlayers.client.layer.XYZOptions; import org.gwtopenmaps.openlayers.client.popup.FramedCloud; import org.gwtopenmaps.openlayers.client.popup.Popup; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.MenuBar; import com.google.gwt.user.client.ui.MenuItem; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.Widget; import com.mecatran.otp.gwt.client.PlannerResources; import com.mecatran.otp.gwt.client.i18n.I18nUtils; import com.mecatran.otp.gwt.client.model.ItineraryBean; import com.mecatran.otp.gwt.client.model.LocationBean; import com.mecatran.otp.gwt.client.model.POIBean; import com.mecatran.otp.gwt.client.model.TransportMode; import com.mecatran.otp.gwt.client.model.Wgs84BoundsBean; import com.mecatran.otp.gwt.client.model.Wgs84LatLonBean; import com.mecatran.otp.gwt.client.proxies.POISource; import com.mecatran.otp.gwt.client.view.POIUtils.POILayerType; public class OpenLayersPlannerMapWidget extends Composite implements PlannerMapWidget { private static final Projection WGS84_PROJECTION = new Projection( "EPSG:4326"); protected final static String POPUP_CONTENT_KEY = "__POPUP_CONTENT__"; protected final static String POPUP_CLASS_KEY = "__POPUP_CLASS__"; protected final static String WAYPOINT_TYPE_KEY = "__WAYPOINT_TYPE__"; protected final static String WAYPOINT_DRAGGABLE_KEY = "__WAYPOINT_DRAGGABLE__"; private MapListener mapListener; private MapWidget mapWidget; private Map map; private Projection mapProjection; private Vector waypointLayer, itineraryLayer, previewItineraryLayer; private Popup infoPopup; private DragFeature dragFeature; private OpenLayersWaypoint departureWaypoint; private OpenLayersWaypoint arrivalWaypoint; private OpenLayersWaypoint popupMarker; private OpenLayersItinerary selectedItinerary; private OpenLayersItinerary previewItinerary; private PopupPanel popupMenu; private LonLat popupMenuPosition; private Wgs84BoundsBean bounds; private Layer cycleLayer; private Layer stopsLayer; private Layer routesLayer; private java.util.Map<POILayerType, OpenLayersPOILayer> poiLayers = new HashMap<POILayerType, OpenLayersPOILayer>(); @SuppressWarnings("unused") public OpenLayersPlannerMapWidget() { // create some MapOptions MapOptions defaultMapOptions = new MapOptions(); defaultMapOptions.setNumZoomLevels(16); defaultMapOptions.setDisplayProjection(WGS84_PROJECTION); // Create a MapWidget mapWidget = new MapWidget("100%", "100%", defaultMapOptions); map = mapWidget.getMap(); // Create map layers // Cloud-made map if (false) { XYZOptions cloudmadeOptions = new XYZOptions(); cloudmadeOptions.setIsBaseLayer(true); cloudmadeOptions.setSphericalMercator(true); XYZ cloudmadeLayer = new XYZ( "Cloudmade", "http://tile.cloudmade.com/YOUR_KEY_HERE/99823/256/${z}/${x}/${y}.png", cloudmadeOptions); map.addLayer(cloudmadeLayer); } // MapQuest open OSMOptions mapQuestOptions = new OSMOptions(); mapQuestOptions .setAttribution("Tiles Courtesy of <a href='http://www.mapquest.com/' target='_blank'>MapQuest</a> <img src='http://developer.mapquest.com/content/osm/mq_logo.png'>. Map data provided by <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> and contributors, <a href='http://wiki.openstreetmap.org/wiki/Legal_FAQ'>ODbL</a>."); OSM mapQuest = new OSM( I18nUtils.tr("layer.mapquest.street"), new String[] { "http://otile1.mqcdn.com/tiles/1.0.0/map/${z}/${x}/${y}.jpg", "http://otile2.mqcdn.com/tiles/1.0.0/map/${z}/${x}/${y}.jpg", "http://otile3.mqcdn.com/tiles/1.0.0/map/${z}/${x}/${y}.jpg", "http://otile4.mqcdn.com/tiles/1.0.0/map/${z}/${x}/${y}.jpg" }, mapQuestOptions); mapQuest.setIsBaseLayer(true); map.addLayer(mapQuest); // OSM Mapnik OSM mapnik = OSM.Mapnik(I18nUtils.tr("layer.osm.street")); mapnik.setIsBaseLayer(true); map.addLayer(mapnik); // OpenCycleMap / Transport / Outdoor XYZOptions thunderforestOptions = new XYZOptions(); thunderforestOptions.setIsBaseLayer(true); thunderforestOptions.setSphericalMercator(true); thunderforestOptions .setAttribution("Copyright CC-BY-SA 2.0 <a href='www.openstreetmap.org'>OpenStreetMap</a> and <a href='www.thunderforest.com'>Thunderforest</a>"); XYZ transportLayer = new XYZ( "Transport", new String[] { "http://a.tile2.opencyclemap.org/transport/${z}/${x}/${y}.png", "http://b.tile2.opencyclemap.org/transport/${z}/${x}/${y}.png", "http://c.tile2.opencyclemap.org/transport/${z}/${x}/${y}.png" }, thunderforestOptions); map.addLayer(transportLayer); // Outdoor XYZ outdoorLayer = new XYZ( "Outdoor", new String[] { "http://a.tile.thunderforest.com/outdoors/${z}/${x}/${y}.png", "http://b.tile.thunderforest.com/outdoors/${z}/${x}/${y}.png", "http://c.tile.thunderforest.com/outdoors/${z}/${x}/${y}.png" }, thunderforestOptions); map.addLayer(outdoorLayer); // Landscape XYZ landscapeLayer = new XYZ( "Landscape", new String[] { "http://a.tile3.opencyclemap.org/landscape/${z}/${x}/${y}.png", "http://b.tile3.opencyclemap.org/landscape/${z}/${x}/${y}.png", "http://c.tile3.opencyclemap.org/landscape/${z}/${x}/${y}.png" }, thunderforestOptions); map.addLayer(landscapeLayer); // Cyclemap cycleLayer = OSM.CycleMap(I18nUtils.tr("layer.osm.cycle")); cycleLayer.setIsBaseLayer(true); map.addLayer(cycleLayer); mapProjection = new Projection(map.getProjection()); // Create vector layers List<Vector> allLayers = new ArrayList<Vector>(); // Preview itinerary VectorOptions previewItineraryLayerOptions = new VectorOptions(); previewItineraryLayerOptions.setDisplayInLayerSwitcher(false); previewItineraryLayer = new Vector("Itinerary preview", previewItineraryLayerOptions); previewItineraryLayer.setIsVisible(true); allLayers.add(previewItineraryLayer); map.addLayer(previewItineraryLayer); // Waypoint layer VectorOptions vectorLayerOptions = new VectorOptions(); vectorLayerOptions.setDisplayInLayerSwitcher(false); vectorLayerOptions.setLayerOpacity(1.0); waypointLayer = new Vector("Waypoints", vectorLayerOptions); waypointLayer.setIsVisible(true); map.addLayer(waypointLayer); allLayers.add(waypointLayer); // Itinerary layer itineraryLayer = new Vector("Itinerary", vectorLayerOptions); itineraryLayer.setIsVisible(true); map.addLayer(itineraryLayer); allLayers.add(itineraryLayer); // Pre-create POI layers for (POILayerType layerType : POILayerType.values()) { // Lazy-create a new layer OpenLayersPOILayer poiLayer = new OpenLayersPOILayer(map, layerType); poiLayers.put(layerType, poiLayer); allLayers.add(poiLayer.getLayer()); } // Drag feature for departure/arrival DragFeatureOptions dragFeatureOptions = new DragFeatureOptions(); dragFeatureOptions.onComplete(new DragFeatureListener() { @Override public void onDragEvent(VectorFeature vectorFeature, Pixel pixel) { handleDrag(vectorFeature); } }); dragFeatureOptions.onStart(new DragFeatureListener() { @Override public void onDragEvent(VectorFeature vectorFeature, Pixel pixel) { if (!vectorFeature.getAttributes().getAttributeAsBoolean( WAYPOINT_DRAGGABLE_KEY)) { dragFeature.deactivate(); dragFeature.activate(); } } }); dragFeature = new DragFeature(waypointLayer, dragFeatureOptions); map.addControl(dragFeature); dragFeature.activate(); // Select feature for click on POI/waypoint/mode icon SelectFeatureOptions selectFeatureOptions = new SelectFeatureOptions(); selectFeatureOptions.clickFeature(new ClickFeatureListener() { @Override public void onFeatureClicked(VectorFeature vectorFeature) { handleFeatureSelection(vectorFeature); } }); SelectFeature selectFeature = new SelectFeature( allLayers.toArray(new Vector[0]), selectFeatureOptions); map.addControl(selectFeature); selectFeature.activate(); // Create departure/arrival waypoints departureWaypoint = new OpenLayersWaypoint(waypointLayer, "departure", true, PlannerResources.INSTANCE.flagmapDeparturePng(), 1.0); arrivalWaypoint = new OpenLayersWaypoint(waypointLayer, "arrival", true, PlannerResources.INSTANCE.flagmapArrivalPng(), 1.0); popupMarker = new OpenLayersWaypoint(waypointLayer, "popup", false, PlannerResources.INSTANCE.flagmapWaypointPng(), 1.0); // Lets add some default controls to the map // + sign in the upperright corner to display the layer switcher map.addControl(new LayerSwitcher()); // + sign in the lowerright to display the overviewmap map.addControl(new OverviewMap()); // Display the scaleline map.addControl(new ScaleLine()); // Add map handlers map.addMapClickListener(new MapClickListener() { @Override public void onClick(MapClickEvent mapClickEvent) { handleClickOnMap(mapClickEvent.getLonLat(), mapClickEvent.getPixel()); } }); map.addMapMoveListener(new MapMoveListener() { @Override public void onMapMove(MapMoveEvent eventObject) { hidePopupMenu(); } }); map.addMapZoomListener(new MapZoomListener() { @Override public void onMapZoom(MapZoomEvent eventObject) { hidePopupMenu(); } }); // Create popup menu popupMenu = new PopupPanel(true); popupMenu.setWidth("15em"); popupMenu.addStyleName("popup-menu"); MenuBar menuBar = new MenuBar(true); popupMenu.add(menuBar); MenuItem startHereMenuItem = new MenuItem(I18nUtils.tr("start.here"), new Command() { @Override public void execute() { startHere(); } }); startHereMenuItem.addStyleName("departure-icon"); menuBar.addItem(startHereMenuItem); MenuItem endHereMenuItem = new MenuItem(I18nUtils.tr("end.here"), new Command() { @Override public void execute() { endHere(); } }); endHereMenuItem.addStyleName("arrival-icon"); menuBar.addItem(endHereMenuItem); MenuItem zoomInMenuItem = new MenuItem(I18nUtils.tr("zoom.in"), new Command() { @Override public void execute() { map.setCenter(map.getCenter(), map.getZoom() + 1); } }); zoomInMenuItem.addStyleName("zoomin-icon"); menuBar.addItem(zoomInMenuItem); MenuItem zoomOutMenuItem = new MenuItem(I18nUtils.tr("zoom.out"), new Command() { @Override public void execute() { map.setCenter(map.getCenter(), map.getZoom() - 1); } }); zoomOutMenuItem.addStyleName("zoomout-icon"); menuBar.addItem(zoomOutMenuItem); MenuItem centerMapMenuItem = new MenuItem(I18nUtils.tr("center.map"), new Command() { @Override public void execute() { map.setCenter(popupMarker.getLonLat()); } }); centerMapMenuItem.addStyleName("centermap-icon"); menuBar.addItem(centerMapMenuItem); popupMenu.addCloseHandler(new CloseHandler<PopupPanel>() { @Override public void onClose(CloseEvent<PopupPanel> event) { hidePopupMenu(); } }); // Force the map to fall behind popups mapWidget.getElement().getFirstChildElement().getStyle().setZIndex(0); initWidget(mapWidget); } @Override public Widget getAsWidget() { return this; } @Override public void setMapListener(MapListener mapListener) { this.mapListener = mapListener; } @Override public void setBounds(Wgs84BoundsBean aBounds) { bounds = aBounds; Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { map.updateSize(); Bounds extent = convertBounds(bounds); map.setMaxExtent(extent); map.setRestrictedExtent(extent); map.zoomToExtent(extent); // Zoom a bit inside as fitting bounds will be too large map.setCenter(map.getCenter(), map.getZoom() + 1); } }); } @Override public void updateSize() { map.updateSize(); } @Override public void showItineraryOnMap(ItineraryBean itinerary) { // Close popups (info and menu) hidePopupMenu(); if (infoPopup != null) { map.removePopup(infoPopup); } // Remove existing itinerary if (selectedItinerary != null) { selectedItinerary.removeFromLayer(); selectedItinerary = null; } if (itinerary == null) { // No itinerary: show departure/arrival flag departureWaypoint.show(); arrivalWaypoint.show(); itineraryLayer.redraw(); // Reset stop/route layer visibility to what it was if (stopsLayer != null) stopsLayer.setOpacity(1.0f); if (routesLayer != null) routesLayer.setOpacity(1.0f); for (OpenLayersPOILayer poiLayer : poiLayers.values()) poiLayer.setOpacity(1.0f); } else { if (stopsLayer != null) stopsLayer.setOpacity(0.2f); if (routesLayer != null) routesLayer.setOpacity(0.2f); for (OpenLayersPOILayer poiLayer : poiLayers.values()) poiLayer.setOpacity(0.2f); // Show selected itinerary on map departureWaypoint.hide(); arrivalWaypoint.hide(); if (itinerary.getBounds() != null) { Bounds extent = convertBounds(itinerary.getBounds()); map.zoomToExtent(extent); } selectedItinerary = new OpenLayersItinerary(itineraryLayer, mapProjection, itinerary, 1.0); } } @Override public void previewItineraryOnMap(ItineraryBean itinerary) { // Remove existing preview if (previewItinerary != null) { previewItinerary.removeFromLayer(); previewItinerary = null; } if (itinerary != null) { previewItinerary = new OpenLayersItinerary(previewItineraryLayer, mapProjection, itinerary, 0.4); } } @Override public void previewLocation(LocationBean location) { if (location == null) { // TODO - popup marker seems to be broken here... // Only use info popup for now. // if (popupMarker.isSet()) { // popupMarker.hide(); // } if (infoPopup != null) { map.removePopup(infoPopup); infoPopup = null; } } else { LonLat lonLat = convertLonLat(location.getLocation()); if (infoPopup != null) { map.removePopup(infoPopup); } infoPopup = new FramedCloud("infoPopup", lonLat, new Size(150, 100), wrapPopupHtml("instructions-popup", "<h3>" + location.getAddress() + "</h3>"), null, true); infoPopup.setAutoSize(true); infoPopup.setPanMapIfOutOfView(true); map.addPopup(infoPopup); infoPopup.show(); // popupMarker.moveTo(lonLat); // popupMarker.show(); map.panTo(lonLat); } } @Override public void updateDeparturePosition(Wgs84LatLonBean departurePosition, boolean autoPan) { if (departurePosition == null) { departureWaypoint.hide(); } else { LonLat lonLat = convertLonLat(departurePosition); departureWaypoint.moveTo(lonLat); if (autoPan) map.panTo(lonLat); } waypointLayer.redraw(); } @Override public void updateArrivalPosition(Wgs84LatLonBean arrivalPosition, boolean autoPan) { if (arrivalPosition == null) { arrivalWaypoint.hide(); } else { LonLat lonLat = convertLonLat(arrivalPosition); arrivalWaypoint.moveTo(lonLat); if (autoPan) map.panTo(lonLat); } } @Override public void highlightItineraryStep(String instructionsHtml, Wgs84LatLonBean location) { if (infoPopup != null) { map.removePopup(infoPopup); } infoPopup = new FramedCloud("infoPopup", convertLonLat(location), new Size(200, 200), wrapPopupHtml("instructions-popup", instructionsHtml), null, true); infoPopup.setAutoSize(true); infoPopup.setPanMapIfOutOfView(true); map.addPopup(infoPopup); infoPopup.show(); } @Override public void focusDeparture() { if (departureWaypoint.isSet()) map.panTo(departureWaypoint.getLonLat()); waypointLayer.redraw(); } @Override public void focusArrival() { if (arrivalWaypoint.isSet()) map.panTo(arrivalWaypoint.getLonLat()); waypointLayer.redraw(); } @Override public void switchMapBackground(Set<TransportMode> modes) { if (modes.contains(TransportMode.BICYCLE) || modes.contains(TransportMode.BICYCLE_RENTAL)) map.setBaseLayer(cycleLayer); } @Override public void updatePOIs(POISource source, java.util.Map<String, POIBean> pois) { // Group POI by layers as a source can impact many layers java.util.Map<POILayerType, java.util.Map<String, POIBean>> poisPerLayer = new HashMap<POILayerType, java.util.Map<String, POIBean>>(); for (POIBean poi : pois.values()) { POILayerType layerType = POIUtils.mapPoiTypeToLayerType(poi .getType()); java.util.Map<String, POIBean> poisForLayer = poisPerLayer .get(layerType); if (poisForLayer == null) { poisForLayer = new HashMap<String, POIBean>(); poisPerLayer.put(layerType, poisForLayer); } poisForLayer.put(poi.getId(), poi); } // For each POI layer group, update layer for (java.util.Map.Entry<POILayerType, java.util.Map<String, POIBean>> kv : poisPerLayer .entrySet()) { POILayerType layerType = kv.getKey(); java.util.Map<String, POIBean> poisForLayer = kv.getValue(); OpenLayersPOILayer poiLayer = poiLayers.get(layerType); poiLayer.updatePOIList(source.getUniqueId(), poisForLayer); } } private LonLat convertLonLat(Wgs84LatLonBean latLon) { LonLat lonLat = new LonLat(latLon.getLon(), latLon.getLat()); lonLat.transform(WGS84_PROJECTION.getProjectionCode(), mapProjection.getProjectionCode()); return lonLat; } private Wgs84LatLonBean convertLatLng(LonLat lonLat) { LonLat lonLat2 = new LonLat(lonLat.lon(), lonLat.lat()); lonLat2.transform(mapProjection.getProjectionCode(), WGS84_PROJECTION.getProjectionCode()); return new Wgs84LatLonBean(lonLat2.lat(), lonLat2.lon()); } private Bounds convertBounds(Wgs84BoundsBean bounds) { Bounds extent = new Bounds(bounds.getMinLon(), bounds.getMinLat(), bounds.getMaxLon(), bounds.getMaxLat()); extent.transform(WGS84_PROJECTION, mapProjection); return extent; } private void handleDrag(VectorFeature feature) { if (feature.getAttributes().getAttributeAsString(WAYPOINT_TYPE_KEY) .equals("departure")) { mapListener.onStartPointSelected(convertLatLng(feature .getCenterLonLat())); } else if (feature.getAttributes() .getAttributeAsString(WAYPOINT_TYPE_KEY).equals("arrival")) { mapListener.onEndPointSelected(convertLatLng(feature .getCenterLonLat())); } } private void hidePopupMenu() { if (popupMenu.isShowing()) popupMenu.hide(); if (popupMarker.isSet()) { popupMarker.hide(); } } private void handleClickOnMap(LonLat lonLat, Pixel pixel) { if (popupMenu.isShowing()) { hidePopupMenu(); return; } popupMarker.moveTo(lonLat); popupMenuPosition = lonLat; popupMenu.setPopupPosition(this.getAbsoluteLeft() + pixel.x() + 2, this.getAbsoluteTop() + pixel.y() + 2); popupMenu.show(); } private void startHere() { hidePopupMenu(); mapListener.onStartPointSelected(convertLatLng(popupMenuPosition)); } private void endHere() { hidePopupMenu(); mapListener.onEndPointSelected(convertLatLng(popupMenuPosition)); } private void handleFeatureSelection(VectorFeature feature) { if (infoPopup != null) { map.removePopup(infoPopup); infoPopup = null; } String popupContent = feature.getAttributes().getAttributeAsString( POPUP_CONTENT_KEY); String popupClass = feature.getAttributes().getAttributeAsString( POPUP_CLASS_KEY); if (popupContent != null) { infoPopup = new FramedCloud("infoPopup", feature.getCenterLonLat(), new Size(200, 200), wrapPopupHtml(popupClass, popupContent), null, true); infoPopup.setAutoSize(true); infoPopup.setBorder("1px black solid"); infoPopup.setPanMapIfOutOfView(true); map.addPopup(infoPopup); infoPopup.show(); } } private String wrapPopupHtml(String popupClass, String html) { return "<div class='generic-popup" + (popupClass == null ? "" : " " + popupClass) + "'>" + html + "</div>"; } }