/* * This is part of Geomajas, a GIS framework, http://www.geomajas.org/. * * Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium. * * The program is available in open source according to the GNU Affero * General Public License. All contributions in this program are covered * by the Geomajas Contributors License Agreement. For full licensing * details, see LICENSE.txt in the project root. */ package org.geomajas.widget.featureinfo.client.action.toolbar; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.geomajas.command.dto.SearchByLocationRequest; import org.geomajas.command.dto.SearchByLocationResponse; import org.geomajas.configuration.AbstractAttributeInfo; import org.geomajas.configuration.AbstractReadOnlyAttributeInfo; import org.geomajas.configuration.PrimitiveAttributeInfo; import org.geomajas.configuration.client.ClientVectorLayerInfo; import org.geomajas.geometry.Coordinate; import org.geomajas.gwt.client.command.AbstractCommandCallback; import org.geomajas.gwt.client.command.GwtCommand; import org.geomajas.gwt.client.command.GwtCommandDispatcher; import org.geomajas.gwt.client.controller.listener.AbstractListener; import org.geomajas.gwt.client.controller.listener.ListenerEvent; import org.geomajas.gwt.client.map.layer.Layer; import org.geomajas.gwt.client.map.layer.VectorLayer; import org.geomajas.gwt.client.spatial.Mathlib; import org.geomajas.gwt.client.spatial.WorldViewTransformer; import org.geomajas.gwt.client.spatial.geometry.Point; import org.geomajas.gwt.client.util.GeometryConverter; import org.geomajas.gwt.client.util.WidgetLayout; import org.geomajas.gwt.client.widget.MapWidget; import org.geomajas.layer.feature.Attribute; import org.geomajas.layer.feature.Feature; import org.geomajas.widget.featureinfo.client.FeatureInfoMessages; import org.geomajas.widget.featureinfo.client.util.FitLayout; import org.geomajas.widget.featureinfo.client.util.FitSetting; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.Timer; import com.smartgwt.client.widgets.Canvas; import com.smartgwt.client.widgets.Img; /** * Listener for feature tooltip on mouse over. * * @author Kristof Heirwegh * @author Oliver May * @author Wout Swartenbroekx */ public class TooltipOnMouseoverListener extends AbstractListener { private static final FeatureInfoMessages MESSAGES = GWT.create(FeatureInfoMessages.class); private Canvas tooltip; private int pixelTolerance = FitSetting.tooltipPixelTolerance; private int minPixelMove = FitSetting.tooltipMinimalPixelMove; private final int showDelay = FitSetting.tooltipShowDelay; private int maxLabelCount = FitSetting.tooltipMaxLabelCount; private boolean showEmptyResults = FitSetting.tooltipShowEmptyResultMessage; private Coordinate lastPosition; // screen private Coordinate currentPosition; // screen private Coordinate worldPosition; // world !! private Timer timer; private int widest = 10; private int count; private final MapWidget mapWidget; private final List<String> layersToExclude = new ArrayList<String>(); private boolean useFeatureDetail; private static final String CSS = "<style>.tblcLayerLabel {font-size: 0.9em; font-weight: bold;} " + ".tblcFeatureLabelList {margin: 0; padding-left: 20px;} " + ".tblcFeatureLabel {margin: 0; font-size: 0.9em; font-style: italic;} " + ".tblcMore {padding-top: 5px; font-size: 0.9em; font-style: italic;}</style>"; /** * Constructor. * * @param mapWidget map widget */ public TooltipOnMouseoverListener(MapWidget mapWidget) { super(); this.mapWidget = mapWidget; } /** * Initialization. */ public void onActivate() { onDeactivate(); } /** Clean everything up. */ public void onDeactivate() { destroyTooltip(); if (timer != null) { timer.cancel(); } } @Override public void onMouseMove(ListenerEvent event) { // do not use getRelative(mapwidget.getElement()) -- it reinitializes the map... currentPosition = event.getClientPosition(); if (lastPosition == null) { lastPosition = currentPosition; } else { double distance = currentPosition.distance(lastPosition); if (distance > minPixelMove) { lastPosition = currentPosition; worldPosition = event.getWorldPosition(); if (isShowing()) { destroyTooltip(); } // place new tooltip after some time if (timer == null) { timer = new Timer() { public void run() { showTooltip(); } }; timer.schedule(showDelay); } else { timer.cancel(); timer.schedule(showDelay); } } } } @Override public void onMouseOut(ListenerEvent event) { // when label is repositioned in corner it can be put under mouse, which (falsely) generates a mouseOutEvent if (!overlapsTooltip((int) event.getClientPosition().getX(), (int) event.getClientPosition().getY())) { onDeactivate(); } } // ---------------------------------------------------------- private boolean isShowing() { return (tooltip != null); } private void showTooltip() { if (!isShowing()) { createTooltip((int) currentPosition.getX() + FitLayout.tooltipOffsetX, (int) currentPosition.getY() + FitLayout.tooltipOffsetY, null); getData(); } } private void writeLayerStart(StringBuilder sb, String label) { sb.append("<span class='tblcLayerLabel'>"); sb.append(label); sb.append("</span><ul class='tblcFeatureLabelList'>"); } private void writeFeature(StringBuilder sb, String label) { sb.append("<li class='tblcFeatureLabel'>"); sb.append(label); sb.append("</li>"); } private void writeLayerEnd(StringBuilder sb) { sb.append("</ul>"); } private void writeNone(StringBuilder sb) { sb.append("<span class='tblcMore'>("); sb.append(MESSAGES.tooltipOnMouseoverNoResult()); sb.append(")</span>"); } private void writeTooMany(StringBuilder sb, int tooMany) { sb.append("<span class='tblcMore'>"); sb.append(Integer.toString(tooMany)); sb.append("</span>"); } private void setTooltipData(Coordinate coordUsedForRetrieval, Map<String, List<Feature>> featureMap) { if (coordUsedForRetrieval.equals(worldPosition) && tooltip != null) { StringBuilder sb = new StringBuilder(); sb.append(CSS); widest = 10; count = 0; for (Layer<?> layer : mapWidget.getMapModel().getLayers()) { if (featureMap.containsKey(layer.getId()) && useLayer(layer.getId())) { List<Feature> features = featureMap.get(layer.getId()); if (features.size() > 0) { if (useFeatureDetail) { setTooltipDetailData(sb, layer, features); } else { setTooltipDefaultData(sb, layer, features); } } } } int left = tooltip.getLeft(); int top = tooltip.getTop(); destroyTooltip(); if (count > maxLabelCount) { writeTooMany(sb, count - maxLabelCount); } else if (count == 0 && showEmptyResults) { writeNone(sb); } else if (count == 0) { return; } Canvas content = new Canvas(); content.setContents(sb.toString()); int width = (int) (widest * 4.8) + 40; if (width < 150) { width = 150; } content.setWidth(width); content.setAutoHeight(); content.setMargin(5); createTooltip(left, top, content); } // else - mouse moved between request and data retrieval } private void setTooltipDefaultData(StringBuilder sb, Layer<?> layer, List<Feature> features) { if (count < maxLabelCount) { writeLayerStart(sb, layer.getLabel()); widest = updateTooltipSize(widest, layer.getLabel()); for (Feature feature : features) { if (count < maxLabelCount) { String label = feature.getLabel(); writeFeature(sb, label); widest = updateTooltipSize(widest, label); } count++; } writeLayerEnd(sb); } else { count += features.size(); } } private void setTooltipDetailData(StringBuilder sb, Layer<?> layer, List<Feature> features) { for (Feature feature : features) { if (count < maxLabelCount) { String featureLabel = layer.getLabel() + " " + feature.getId(); widest = updateTooltipSize(widest, featureLabel); writeLayerStart(sb, featureLabel); for (Entry<String, Attribute> entry : feature .getAttributes().entrySet()) { if (isIdentifying(entry.getKey(), layer)) { String label = getEntryLabel(entry.getKey(), layer) + ": " + entry.getValue().toString(); writeFeature(sb, label); widest = updateTooltipSize(widest, label); } } writeLayerEnd(sb); count++; } } } private String getEntryLabel(String key, Layer layer) { ClientVectorLayerInfo c = (ClientVectorLayerInfo) layer.getLayerInfo(); for (AbstractAttributeInfo a : c.getFeatureInfo().getAttributes()) { if (a.getName().equalsIgnoreCase(key)) { return ((PrimitiveAttributeInfo) a).getLabel(); } } return null; } private boolean isIdentifying(String key, Layer layer) { if (layer instanceof VectorLayer) { ClientVectorLayerInfo c = (ClientVectorLayerInfo) layer.getLayerInfo(); for (AbstractAttributeInfo a : c.getFeatureInfo().getAttributes()) { if (a.getName().equalsIgnoreCase(key)) { return a instanceof AbstractReadOnlyAttributeInfo && ((AbstractReadOnlyAttributeInfo) a).isIdentifying(); } } } return false; } private boolean useLayer(String id) { return !layersToExclude.contains(id); } protected int updateTooltipSize(int widest, String label) { int size = getLabelSize(label); if (widest < size) { widest = size; } return widest; } protected int getLabelSize(String label) { if (label == null) { return 0; } else if (label.length() < 20) { return label.length(); // don't bother } else { String[] subLab = label.split("<br ?/>"); if (subLab.length > 1) { int max = 0; for (String sub : subLab) { if (sub.length() > max) { max = sub.length(); } } return max; } else { return label.length(); } } } private void getData() { Point point = mapWidget.getMapModel().getGeometryFactory().createPoint(worldPosition); final Coordinate coordUsedForRetrieval = worldPosition; SearchByLocationRequest request = new SearchByLocationRequest(); request.setLocation(GeometryConverter.toDto(point)); request.setCrs(mapWidget.getMapModel().getCrs()); request.setQueryType(SearchByLocationRequest.QUERY_INTERSECTS); int layersToSearch = SearchByLocationRequest.SEARCH_ALL_LAYERS; request.setSearchType(layersToSearch); request.setBuffer(calculateBufferFromPixelTolerance()); request.setFeatureIncludes(GwtCommandDispatcher.getInstance().getLazyFeatureIncludesSelect()); for (Layer<?> layer : mapWidget.getMapModel().getLayers()) { if (layer.isShowing() && layer instanceof VectorLayer) { request.addLayerWithFilter(layer.getId(), layer.getServerLayerId(), ((VectorLayer) layer).getFilter()); } } GwtCommand commandRequest = new GwtCommand(SearchByLocationRequest.COMMAND); commandRequest.setCommandRequest(request); GwtCommandDispatcher.getInstance().execute(commandRequest, new AbstractCommandCallback<SearchByLocationResponse>() { public void execute(SearchByLocationResponse commandResponse) { setTooltipData(coordUsedForRetrieval, commandResponse.getFeatureMap()); } }); } private void createTooltip(int x, int y, Canvas content) { if (mapWidget != null) { tooltip = new Canvas(); tooltip.setBackgroundColor("white"); tooltip.setShowShadow(true); tooltip.setOpacity(85); tooltip.setBorder("thin solid #AAAAAA"); if (content != null) { tooltip.addChild(content); } else { tooltip.addChild(getLoadingImg()); } tooltip.setAutoWidth(); tooltip.setAutoHeight(); tooltip.hide(); tooltip.draw(); // need this to get correct size of tooltip placeTooltip(x, y); tooltip.show(); } } private void placeTooltip(int x, int y) { if (tooltip != null) { int realx = x; int realy = y; int tooltipWidth = tooltip.getRight() - tooltip.getLeft(); int tooltipHeight = tooltip.getBottom() - tooltip.getTop(); int overlapX = (x + (tooltipWidth)) - mapWidget.getRight(); int overlapY = (y + (tooltipHeight)) - mapWidget.getBottom(); if (overlapX > 0) { realx -= overlapX; } if (overlapY > 0) { realy -= overlapY; } tooltip.moveTo(realx, realy); } } private void destroyTooltip() { if (tooltip != null) { tooltip.destroy(); tooltip = null; } } private boolean overlapsTooltip(int x, int y) { if (null == tooltip) { return false; } else { return (!(x < tooltip.getLeft() || x > tooltip.getRight() || y < tooltip.getTop() || y > tooltip .getBottom())); } } private double calculateBufferFromPixelTolerance() { WorldViewTransformer transformer = mapWidget.getMapModel().getMapView().getWorldViewTransformer(); Coordinate c1 = transformer.viewToWorld(new Coordinate(0, 0)); Coordinate c2 = transformer.viewToWorld(new Coordinate(pixelTolerance, 0)); return Mathlib.distance(c1, c2); } private Canvas getLoadingImg() { Canvas c = new Canvas(); c.setMargin(4); c.setWidth(26); c.setHeight(26); c.addChild(new Img(WidgetLayout.iconAjaxLoading16, 16, 16)); return c; } /** * Get pixel tolerance. * * @return pixel tolerance */ public int getPixelTolerance() { return pixelTolerance; } /** * Set pixel tolerance. * * @param pixelTolerance pixel tolerance */ public void setPixelTolerance(int pixelTolerance) { this.pixelTolerance = pixelTolerance; } /** * Set if empty results should be displayed as "no results found" or be omitted. * @param show true if empty results should be shown. */ public void setShowEmptyResult(boolean show) { this.showEmptyResults = show; } /** * Set the minimal distance the mouse must move before a new mouse over request is triggered. * @param distance the minimal distance. */ public void setMinimalMoveDistance(int distance) { this.minPixelMove = distance; } /** * Set if tooltip should be filled with feature details instead of label. * * @param useFeatureDetail true to use details of object, false to use default labels */ public void setTooltipUseFeatureDetail(boolean useFeatureDetail) { this.useFeatureDetail = useFeatureDetail; } /** * Set the list of Layer IDs for layers which should not be used to draw the detail tooltip. * * @param layerIds list of Layer IDs */ public void setLayersToExclude(String[] layerIds) { this.layersToExclude.clear(); Collections.addAll(this.layersToExclude, layerIds); } /** * Set the amount of features for which detailed information should be shown. * * @param maxLabelCount the number of features. */ public void setTooltipMaxLabelCount(int maxLabelCount) { this.maxLabelCount = maxLabelCount; } /** * Get max label count. * * @return max label count */ public int getMaxLabelCount() { return this.maxLabelCount; } }