/* * * ============================================================================== * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package wicket.contrib.gmap3; import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.RuntimeConfigurationType; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.html.IHeaderResponse; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.request.Request; import org.apache.wicket.request.cycle.RequestCycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import wicket.contrib.gmap3.api.*; import wicket.contrib.gmap3.mapevent.GMapEventListenerBehavior; import wicket.contrib.gmap3.overlay.GOverlay; import wicket.contrib.gmap3.overlay.OverlayListener; import java.util.*; /** * Wicket component to embed <a href="http://maps.google.com">Google Maps</a> into your pages. * * Understanding ajax-updates with GMap: * There are two situations that are handled by GMap: * <pre> * - map is fully reloaded in ajax-request * -> use methods without AjaxRequestTarget parameter. * - map is not reloaded but should change its state after an ajax-call * -> use methods with AjaxRequestTarget parameter. * </pre> */ public class GMap extends Panel { /** log. */ static final Logger log = LoggerFactory.getLogger(GMap.class); private static final long serialVersionUID = 1L; /** * default map center. */ private GLatLng _center = new GLatLng(0, 0); private boolean _draggingEnabled = true; private boolean _doubleClickZoomEnabled = true; private boolean _scrollWheelZoomEnabled = true; private GMapType _mapType = GMapType.ROADMAP; private int _zoom = 13; private Integer _maxZoom = null; private final Set<GControl> _controls = new HashSet<GControl>(); final List<GOverlay> _overlays = new ArrayList<GOverlay>(); private final WebMarkupContainer _map; private final GInfoWindow _infoWindow; /** * Bounds as retrieved from map.getBounds() */ private GLatLngBounds _getBounds; /** * Bounds used in map.fitBounds(). Be ware of this behavior: * http://code.google.com/p/gmaps-api-issues/issues/detail?id=3117 */ private GLatLngBounds _fitBounds; /* max zoomlevel to use when using fitMarkers. */ private int _boundsMaxZoom = 0; private final OverlayListener _overlayListener; /** * Construct. * * @param id the id */ public GMap(final String id) { this(id, null, new GMapHeaderContributor()); } /** * Construct. * * @param id the id * @param model the model * @param headerContrib the header contrib */ public GMap(final String id, final IModel<?> model, final Behavior headerContrib) { super(id, model); add(headerContrib); add(new Behavior() { @Override public void renderHead(final Component component, final IHeaderResponse response) { response.renderOnDomReadyJavaScript(getJSinit()); } }); _infoWindow = new GInfoWindow(); add(_infoWindow); _map = new WebMarkupContainer("map"); _map.setOutputMarkupId(true); add(_map); _overlayListener = new OverlayListener(); add(_overlayListener); } /** * Instantiates a new g map. * * @param id the id * @param model the model */ public GMap(final String id, final IModel<?> model) { this(id, model, new GMapHeaderContributor()); } public String getMapId() { return _map.getMarkupId(); } /** * On render. * * @see org.apache.wicket.MarkupContainer#onRender(org.apache.wicket.markup.MarkupStream) */ @Override protected void onRender() { super.onRender(); if (RuntimeConfigurationType.DEVELOPMENT.equals(Application.get().getConfigurationType()) && !Application.get().getMarkupSettings().getStripWicketTags()) { log.warn("Application is in DEVELOPMENT mode && Wicket tags are not stripped," + "Some Chrome Versions will not render the GMap." + " Change to DEPLOYMENT mode || turn on Wicket tags stripping." + " See:" + " http://www.nabble.com/Gmap2-problem-with-Firefox-3.0-to18137475.html."); } } /** * Fix for layout bug when map is loaded in hidden div (like in modal window). See * http://code.google.com/p/gmaps-api-issues/issues/detail?id=1448. Clients should call this method in * onBeforeRender. */ public void repaintMap() { if (AjaxRequestTarget.get() != null) { String js = "google.maps.event.trigger(" + getJsReference() + ".map, 'resize');"; js += getJSsetCenter(getCenter()); if (_fitBounds != null) { // if not using fit bounds you must call setZoom to force correct zoom level js += getJSfitBounds(getFitBounds(), _boundsMaxZoom); } else { js += getJSsetZoom(getZoom()); } AjaxRequestTarget.get().appendJavaScript(js); } } /** * Add a control. * * @param control control to add * @return This */ @ReviewPending // remove when method is tested public GMap addControl(final GControl control) { _controls.add(control); return this; } /** * Adds the control. * * Call this method inside ajax request when the map itself is not added to the target. * * @param control the control * @param target the target * @return the g map */ public GMap addControl(final GControl control, final AjaxRequestTarget target) { addControl(control); target.appendJavaScript(control.getJSadd(this)); return this; } /** * Remove a control. * * @param control control to remove * @return This */ @ReviewPending // remove when method is tested public GMap removeControl(final GControl control) { _controls.remove(control); return this; } /** * Remove a control. * * Call this method inside ajax request when the map itself is not added to the target. * * @param control control to remove * @param target the target * @return This */ @ReviewPending // remove when method is tested public GMap removeControl(final GControl control, final AjaxRequestTarget target) { removeControl(control); target.appendJavaScript(control.getJSremove(this)); return this; } /** * Add an overlay. * * @param overlay overlay to add * @return This */ public GMap addOverlay(final GOverlay overlay) { _overlays.add(overlay); overlay.setParent(this); return this; } /** * Add an overlay. * * Call this method inside ajax request when the map itself is not added to the target. * * @param overlay overlay to add * @param target the target * @return This */ public GMap addOverlay(final GOverlay overlay, final AjaxRequestTarget target) { addOverlay(overlay); target.appendJavaScript(overlay.getJS()); return this; } /** * Remove an overlay. * * @param overlay overlay to remove * @return This */ @ReviewPending // remove when method is tested public GMap removeOverlay(final GOverlay overlay) { while (_overlays.contains(overlay)) { _overlays.remove(overlay); } overlay.setParent(null); return this; } /** * Remove an overlay. * * Call this method inside ajax request when the map itself is not added to the target. * * @param overlay overlay to remove * @param target the target * @return This */ @ReviewPending // remove when method is tested public GMap removeOverlay(final GOverlay overlay, final AjaxRequestTarget target) { while (_overlays.contains(overlay)) { _overlays.remove(overlay); } target.appendJavaScript(overlay.getJSremove()); overlay.setParent(null); return this; } /** * Clear all overlays. * * @return This */ public GMap removeAllOverlays() { for (final GOverlay overlay : _overlays) { overlay.setParent(null); } _overlays.clear(); return this; } /** * Clear all overlays. * * Call this method inside ajax request when the map itself is not added to the target. * * @param target the target * @return This */ public GMap removeAllOverlays(final AjaxRequestTarget target) { removeAllOverlays(); target.appendJavaScript(getJSinvoke("clearOverlays()")); return this; } @ReviewPending // remove when method is tested public List<GOverlay> getOverlays() { return Collections.unmodifiableList(_overlays); } /** * Clear all listeners. * * @param target the target * @return the g map */ @ReviewPending // remove when method is tested public GMap clearAllListeners(final AjaxRequestTarget target) { target.appendJavaScript(getJSclearAllListeners()); return this; } @ReviewPending // remove when method is tested public Set<GControl> getControls() { return Collections.unmodifiableSet(_controls); } public GLatLngBounds getBounds() { return _getBounds; } public GLatLngBounds getFitBounds() { return _fitBounds; } /** * <p> * Makes the map zoom out and centre around all the GLatLng points in markersToShow. * <p> * Big ups to Doug Leeper for the code. * * @param markersToShow the points to centre around. * @param maximumZoomLevel the maximum zoom level * @see <a href= "http://www.nabble.com/Re%3A-initial-GMap2-bounds-question-p19886673.html" >Doug's Nabble post</a> */ public void fitMarkers(final List<? extends GLatLng> markersToShow, final int maximumZoomLevel) { if (markersToShow.isEmpty()) { log.warn("Empty list provided to GMap.fitMarkers method."); return; } fitBounds(new GLatLngBounds(markersToShow), maximumZoomLevel); } /** * Be careful. map.fitBounds(map.getBounds()) will actually zoom out. See here: * http://code.google.com/p/gmaps-api-issues/issues/detail?id=3117 * * @param bounds the bounds * @param maximumZoomLevel the maximum zoom level, set to 0 if no max level is to be used */ public void fitBounds(final GLatLngBounds bounds, final int maximumZoomLevel) { _fitBounds = bounds; // set Center so that map will be placed at correct position (avoids short display of Palo Alto location) _center = _fitBounds.getCenter(); _boundsMaxZoom = maximumZoomLevel; } /** * Be careful. map.fitBounds(map.getBounds()) will actually zoom out. See here: * http://code.google.com/p/gmaps-api-issues/issues/detail?id=3117 * * Call this method inside ajax request when the map itself is not added to the target. * * @param bounds the bounds * @param maximumZoomLevel the maximum zoom level, set to 0 if no max level is to be used * @param target the target */ public void fitBounds(final GLatLngBounds bounds, final int maximumZoomLevel, final AjaxRequestTarget target) { fitBounds(bounds, maximumZoomLevel); target.appendJavaScript(getJSfitBounds(_fitBounds, _boundsMaxZoom)); } /** * Sets the dragging enabled. * * @param enabled the new dragging enabled */ @ReviewPending // remove when method is tested public void setDraggingEnabled(final boolean enabled) { _draggingEnabled = enabled; } /** * Sets the dragging enabled. * * Call this method inside ajax request when the map itself is not added to the target. * * @param enabled the enabled * @param target the target */ @ReviewPending // remove when method is tested public void setDraggingEnabled(final boolean enabled, final AjaxRequestTarget target) { setDraggingEnabled(enabled); target.appendJavaScript(getJSsetDraggingEnabled(enabled)); } /** * Checks if is dragging enabled. * * @return true, if is dragging enabled */ public boolean isDraggingEnabled() { return _draggingEnabled; } /** * Sets the double click zoom enabled. * * @param enabled the new double click zoom enabled */ public void setDoubleClickZoomEnabled(final boolean enabled) { _doubleClickZoomEnabled = enabled; } /** * Sets the double click zoom enabled. * * Call this method inside ajax request when the map itself is not added to the target. * * @param enabled the enabled * @param target the target */ public void setDoubleClickZoomEnabled(final boolean enabled, final AjaxRequestTarget target) { setDoubleClickZoomEnabled(enabled); target.appendJavaScript(getJSsetDoubleClickZoomEnabled(enabled)); } /** * Checks if is double click zoom enabled. * * @return true, if is double click zoom enabled */ public boolean isDoubleClickZoomEnabled() { return _doubleClickZoomEnabled; } /** * Sets the scroll wheel zoom enabled. * * @param enabled the new scroll wheel zoom enabled */ public void setScrollWheelZoomEnabled(final boolean enabled) { _scrollWheelZoomEnabled = enabled; } /** * Sets the scroll wheel zoom enabled. * * Call this method inside ajax request when the map itself is not added to the target. * * @param enabled the enabled * @param target the target */ public void setScrollWheelZoomEnabled(final boolean enabled, final AjaxRequestTarget target) { setScrollWheelZoomEnabled(enabled); target.appendJavaScript(getJSsetScrollWheelZoomEnabled(enabled)); } /** * Checks if is scroll wheel zoom enabled. * * @return true, if is scroll wheel zoom enabled */ public boolean isScrollWheelZoomEnabled() { return _scrollWheelZoomEnabled; } /** * Gets the map type. * * @return the map type */ public GMapType getMapType() { return _mapType; } /** * Sets the map type. * * @param mapType the new map type */ public void setMapType(final GMapType mapType) { _mapType = mapType; } /** * Sets the map type. * * Call this method inside ajax request when the map itself is not added to the target. * * @param mapType the map type * @param target the target */ public void setMapType(final GMapType mapType, final AjaxRequestTarget target) { setMapType(mapType); target.appendJavaScript(mapType.getJSsetMapType(GMap.this)); } /** * Gets the zoom. * * @return the zoom */ public int getZoom() { return _zoom; } /** * Sets the zoom. * * @param level the new zoom */ public void setZoom(final int level) { _zoom = level; } /** * Sets the zoom. * * Call this method inside ajax request when the map itself is not added to the target. * * @param level the level * @param target the target */ public void setZoom(final int level, final AjaxRequestTarget target) { setZoom(level); target.appendJavaScript(getJSsetZoom(_zoom)); } public void setMaxZoom(final int level) { _maxZoom = level; } /** * Sets the max zoom. * * Call this method inside ajax request when the map itself is not added to the target. * * @param level the level * @param target the target */ public void setMaxZoom(final int level, final AjaxRequestTarget target) { setMaxZoom(level); target.appendJavaScript(getJSsetMaxZoom(_maxZoom)); } public GLatLng getCenter() { return _center; } /** * Set the center. * * @param center center to set */ public void setCenter(final GLatLng center) { _center = center; // clear fitbounds _fitBounds = null; } /** * Set the center. * * Call this method inside ajax request when the map itself is not added to the target. * * @param center center to set * @param target the target */ public void setCenter(final GLatLng center, final AjaxRequestTarget target) { setCenter(center); target.appendJavaScript(getJSsetCenter(center)); } /** * Changes the center point of the map to the given point. If the point is already visible in the current map view, * change the center in a smooth animation. * * @param center the new center of the map * @param target the target */ @ReviewPending // remove when method is tested public void panTo(final GLatLng center, final AjaxRequestTarget target) { setCenter(center); target.appendJavaScript(getJSpanTo(center)); } /** * Gets the info window. * * @return the info window */ @ReviewPending // remove when method is tested public GInfoWindow getInfoWindow() { return _infoWindow; } /** * Generates the JavaScript used to instantiate this GMap as an JavaScript class on the client side. * * @return The generated JavaScript */ private String getJSinit() { final StringBuffer js = new StringBuffer("new WicketMap('" + _map.getMarkupId() + "');\n"); // js.append(getJSclearAllListeners()); js.append(_overlayListener.getJSinit()); js.append(getJSsetCenter(getCenter())); js.append(getJSsetZoom(getZoom())); js.append(getJSsetDraggingEnabled(_draggingEnabled)); js.append(getJSsetDoubleClickZoomEnabled(_doubleClickZoomEnabled)); js.append(getJSsetScrollWheelZoomEnabled(_scrollWheelZoomEnabled)); js.append(getJSsetMaxZoom(_maxZoom)); js.append(_mapType.getJSsetMapType(this)); js.append(getJSfitBounds(_fitBounds, _boundsMaxZoom)); // Add the controls. // for ( final GControl control : controls ) { // js.append( control.getJSadd( this ) ); // } // Add the overlays. for (final GOverlay overlay : _overlays) { js.append(overlay.getJS()); } for (final Object behavior : getBehaviors(GMapEventListenerBehavior.class)) { js.append(((GMapEventListenerBehavior)behavior).getJSaddListener()); } return js.toString(); } /** * Convenience method for generating a JavaScript call on this GMap with the given invocation. * * @param invocation The JavaScript call to invoke on this GMap. * @return The generated JavaScript. */ public String getJSinvoke(final String invocation) { return getJsReference() + "." + invocation + ";\n"; } /** * Build a reference in JS-Scope. */ public String getJsReference() { return "Wicket.maps['" + _map.getMarkupId() + "']"; } /** * Gets the javascript to call google.map.fitBounds(). * * @param bounds the bounds * @param boundsMaxZoom the bounds max zoom * @return the j sfit markers */ public String getJSfitBounds(final GLatLngBounds bounds, final int boundsMaxZoom) { if (bounds != null) { final StringBuffer buf = new StringBuffer(); if (boundsMaxZoom != 0) { // reading the actual zoom level immediately after fitBounds will not work. it will show false results. // so we use a callback on the bounds_changed event to update the zoom level. final String setZoom = getJsReference() + ".map.setZoom(Math.min(" + getJsReference() + ".map.getZoom()," + boundsMaxZoom + "));\n"; buf.append("google.maps.event.addListenerOnce(" + getJsReference() + ".map, 'bounds_changed', function(evt) { console.log('bounds_changed callback'); " + setZoom + " });"); } buf.append(getJsReference() + ".bounds = new google.maps.LatLngBounds(" + bounds.getSW().getJSconstructor() + "," + bounds.getNE().getJSconstructor() + ");\n"); buf.append(getJsReference() + ".map.fitBounds(" + getJsReference() + ".bounds);\n"); return buf.toString(); } return ""; } /** * Gets the j sset dragging enabled. * * @param enabled the enabled * @return the j sset dragging enabled */ private String getJSsetDraggingEnabled(final boolean enabled) { return getJSinvoke("setDraggingEnabled(" + enabled + ")"); } /** * Gets the j sset double click zoom enabled. * * @param enabled the enabled * @return the j sset double click zoom enabled */ private String getJSsetDoubleClickZoomEnabled(final boolean enabled) { return getJSinvoke("setDoubleClickZoomEnabled(" + enabled + ")"); } /** * Gets the j sset scroll wheel zoom enabled. * * @param enabled the enabled * @return the j sset scroll wheel zoom enabled */ private String getJSsetScrollWheelZoomEnabled(final boolean enabled) { return getJSinvoke("setScrollWheelZoomEnabled(" + enabled + ")"); } /** * Gets the j sset zoom. * * @param zoom the zoom * @return the j sset zoom */ public String getJSsetZoom(final int zoom) { return getJSinvoke("setZoom(" + zoom + ")"); } /** * Gets the j sset max zoom. * * @param maxZoom the max zoom * @return the j sset max zoom */ private String getJSsetMaxZoom(final Integer maxZoom) { if (maxZoom != null) { return getJSinvoke("setMaxZoom(" + maxZoom + ")"); } return ""; } /** * Gets the j sset center. * * @param center the center * @return the j sset center */ public String getJSsetCenter(final GLatLng center) { if (center != null) { return getJSinvoke("setCenter(" + center.getJSconstructor() + ")"); } return ""; } /** * Gets the j span direction. * * @param dx the dx * @param dy the dy * @return the j span direction */ @ReviewPending // remove when method is tested public String getJSpanDirection(final int dx, final int dy) { return getJSinvoke("panDirection(" + dx + "," + dy + ")"); } /** * Gets the j span to. * * @param center the center * @return the j span to */ @ReviewPending // remove when method is tested private String getJSpanTo(final GLatLng center) { return getJSinvoke("panTo(" + center.getJSconstructor() + ")"); } @ReviewPending // remove when method is tested public String getJSzoomOut() { return getJSinvoke("zoomOut()"); } @ReviewPending // remove when method is tested public String getJSzoomIn() { return getJSinvoke("zoomIn()"); } /** * Clear all listeners. * * @param target the target * @return the g map */ @ReviewPending // remove when method is tested public String getJSclearAllListeners() { return getJSinvoke("clearInstanceListeners()"); } /** * Update state from a request to an AJAX target. */ @ReviewPending // remove when method is tested public void update() { final Request request = RequestCycle.get().getRequest(); // Attention: don't use setters as this will result in an endless AJAX request loop _getBounds = GLatLngBounds.parse(request.getRequestParameters().getParameterValue("bounds").toString()); _center = GLatLng.parse(request.getRequestParameters().getParameterValue("center").toString()); _zoom = Integer.parseInt(request.getRequestParameters().getParameterValue("zoom").toString()); _mapType = GMapType.valueOf(request.getRequestParameters().getParameterValue("currentMapType").toString()); // _getBounds != _fitBounds, we have to use workaround for this mismatch // see http://code.google.com/p/gmaps-api-issues/issues/detail?id=3117 _fitBounds = GLatLngBounds.mapToFitBounds(_getBounds); log.debug("update(bounds=[{}], fitBounds=[{}], center=[{}], zoom=[{}], mapType=[{}])", new Object[] { _getBounds, _fitBounds, _center, _zoom, _mapType }); _infoWindow.update(); } @ReviewPending // remove when method is tested public void setOverlays(final List<GOverlay> overlays) { removeAllOverlays(); for (final GOverlay overlay : overlays) { addOverlay(overlay); } } }