/* * Copyright (c) 2012 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.ui.views.styledmap.painter; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Shape; import java.awt.image.BufferedImage; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.swt.graphics.RGB; import org.eclipse.ui.ISelectionListener; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PlatformUI; import org.jdesktop.swingx.image.FastBlurFilter; import org.jdesktop.swingx.mapviewer.GeoPosition; import org.jdesktop.swingx.mapviewer.PixelConverter; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.vividsolutions.jts.geom.Geometry; import de.fhg.igd.geom.BoundingBox; import de.fhg.igd.geom.Point3D; import de.fhg.igd.mapviewer.AbstractTileOverlayPainter; import de.fhg.igd.mapviewer.Refresher; import de.fhg.igd.mapviewer.marker.Marker; import de.fhg.igd.mapviewer.waypoints.GenericWaypoint; import de.fhg.igd.mapviewer.waypoints.GenericWaypointPainter; import de.fhg.igd.mapviewer.waypoints.MarkerWaypointRenderer; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.convert.ConversionUtil; import eu.esdihumboldt.hale.common.instance.helper.PropertyResolver; import eu.esdihumboldt.hale.common.instance.model.DataSet; import eu.esdihumboldt.hale.common.instance.model.Instance; import eu.esdihumboldt.hale.common.instance.model.InstanceCollection; import eu.esdihumboldt.hale.common.instance.model.InstanceReference; import eu.esdihumboldt.hale.common.instance.model.ResourceIterator; import eu.esdihumboldt.hale.common.instance.model.impl.PseudoInstanceReference; import eu.esdihumboldt.hale.common.schema.SchemaSpaceID; import eu.esdihumboldt.hale.common.schema.geometry.GeometryProperty; import eu.esdihumboldt.hale.common.schema.model.SchemaSpace; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.ui.HaleUI; import eu.esdihumboldt.hale.ui.common.service.style.StyleService; import eu.esdihumboldt.hale.ui.common.service.style.StyleServiceListener; import eu.esdihumboldt.hale.ui.geometry.DefaultGeometryUtil; import eu.esdihumboldt.hale.ui.geometry.service.GeometrySchemaServiceListener; import eu.esdihumboldt.hale.ui.selection.InstanceSelection; import eu.esdihumboldt.hale.ui.selection.impl.DefaultInstanceSelection; import eu.esdihumboldt.hale.ui.service.instance.InstanceService; import eu.esdihumboldt.hale.ui.service.instance.InstanceServiceListener; import eu.esdihumboldt.hale.ui.service.schema.SchemaService; import eu.esdihumboldt.hale.ui.util.io.ThreadProgressMonitor; import eu.esdihumboldt.hale.ui.views.styledmap.clip.Clip; import eu.esdihumboldt.hale.ui.views.styledmap.clip.ClipPainter; import eu.esdihumboldt.hale.ui.views.styledmap.util.CRSConverter; import eu.esdihumboldt.hale.ui.views.styledmap.util.CRSDecode; /** * Abstract instance painter implementation based on an {@link InstanceService}. * * @author Simon Templer */ public abstract class AbstractInstancePainter extends GenericWaypointPainter<InstanceReference, InstanceWaypoint> implements InstanceServiceListener, ISelectionListener, ClipPainter { private static final ALogger log = ALoggerFactory.getLogger(AbstractInstancePainter.class); private static final double BUFFER_VALUE = 0.0125; /** * The list of default paths searched in an instance for an instance name. * Search is done in the given order. */ private static final String[] DEFAULT_NAME_PATHS = new String[] { "name", "id", "fid" }; private final InstanceService instanceService; private final DataSet dataSet; private CoordinateReferenceSystem waypointCRS; private Clip clip; private final StyleServiceListener styleListener; private final GeometrySchemaServiceListener geometryListener; private Set<InstanceReference> lastSelected = new HashSet<InstanceReference>(); /** * Create an instance painter. * * @param instanceService the instance service * @param dataSet the data set */ public AbstractInstancePainter(InstanceService instanceService, DataSet dataSet) { super(new MarkerWaypointRenderer<InstanceWaypoint>(), 4); // four worker // threads this.instanceService = instanceService; this.dataSet = dataSet; styleListener = new StyleServiceListener() { @Override public void stylesRemoved(StyleService styleService) { styleRefresh(); } @Override public void stylesAdded(StyleService styleService) { styleRefresh(); } @Override public void styleSettingsChanged(StyleService styleService) { styleRefresh(); } @Override public void backgroundChanged(StyleService styleService, RGB background) { // ignore, background not supported } }; geometryListener = new GeometrySchemaServiceListener() { @Override public void defaultGeometryChanged(TypeDefinition type) { SchemaService ss = PlatformUI.getWorkbench().getService(SchemaService.class); SchemaSpaceID spaceID; switch (getDataSet()) { case TRANSFORMED: spaceID = SchemaSpaceID.TARGET; break; case SOURCE: spaceID = SchemaSpaceID.SOURCE; break; default: throw new IllegalStateException("Illegal data set"); } SchemaSpace schemas = ss.getSchemas(spaceID); if (schemas.getType(type.getName()) != null) { // TODO only update way-points that are affected XXX save // type def in way-point? // XXX for now: recreate all update(null); } } }; } /** * Refresh with style update. */ protected void styleRefresh() { for (InstanceWaypoint wp : iterateWaypoints()) { Marker<? super InstanceWaypoint> marker = wp.getMarker(); if (marker instanceof StyledInstanceMarker) { ((StyledInstanceMarker) marker).resetStyle(); } } refreshAll(); } /** * @see InstanceServiceListener#datasetChanged(DataSet) */ @Override public void datasetChanged(DataSet type) { if (type == dataSet) { update(null); } } /** * Get the CRS for use in way-point bounding boxes. * * @return the way-point CRS */ public CoordinateReferenceSystem getWaypointCRS() { if (waypointCRS == null) { try { waypointCRS = CRSDecode.getCRS(GenericWaypoint.COMMON_EPSG); } catch (Throwable e) { throw new IllegalStateException("Could not decode way-point CRS", e); } } return waypointCRS; } /** * Do a complete update of the way-points. Existing way-points are * discarded. * * @param selection the current selection */ public void update(ISelection selection) { clearWaypoints(); // XXX only mappable type instances for source?! InstanceCollection instances = instanceService.getInstances(dataSet); if (selection != null) { lastSelected = collectReferences(selection); } if (instances.isEmpty()) { return; } final AtomicBoolean updateFinished = new AtomicBoolean(false); IRunnableWithProgress op = new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { String taskName; switch (getDataSet()) { case SOURCE: taskName = "Update source data in map"; break; case TRANSFORMED: default: taskName = "Update transformed data in map"; break; } monitor.beginTask(taskName, IProgressMonitor.UNKNOWN); // add way-points for instances InstanceCollection instances = instanceService.getInstances(dataSet); ResourceIterator<Instance> it = instances.iterator(); try { while (it.hasNext()) { Instance instance = it.next(); InstanceWaypoint wp = createWaypoint(instance, instanceService); if (wp != null) { if (lastSelected.contains(wp.getValue())) { wp.setSelected(true, null); // refresh can be // ignored because // it's done for // addWaypoint } addWaypoint(wp, null); // no refresher, as // refreshAll is executed } } } finally { it.close(); monitor.done(); updateFinished.set(true); } } }; try { ThreadProgressMonitor.runWithProgressDialog(op, false); } catch (Throwable e) { log.error("Error running painter update", e); } HaleUI.waitFor(updateFinished); refreshAll(); } /** * Create a way-point for an instance * * @param instance the instance * @param instanceService the instance service * @return the created way-point or <code>null</code> if */ protected InstanceWaypoint createWaypoint(Instance instance, InstanceService instanceService) { // retrieve instance reference InstanceReference ref = instanceService.getReference(instance);// , // getDataSet()); BoundingBox bb = null; List<GeometryProperty<?>> geometries = new ArrayList<GeometryProperty<?>>( DefaultGeometryUtil.getDefaultGeometries(instance)); ListIterator<GeometryProperty<?>> it = geometries.listIterator(); while (it.hasNext()) { GeometryProperty<?> prop = it.next(); // check if geometry is valid for display in map CoordinateReferenceSystem crs = (prop.getCRSDefinition() == null) ? (null) : (prop.getCRSDefinition().getCRS()); if (crs == null) { // no CRS, can't display in map // remove from list it.remove(); } else { Geometry geometry = prop.getGeometry(); // determine geometry bounding box BoundingBox geometryBB = BoundingBox.compute(geometry); if (geometryBB == null) { // no valid bounding box for geometry it.remove(); } else { try { // get converter to way-point CRS CRSConverter conv = CRSConverter.getConverter(crs, getWaypointCRS()); // convert BB to way-point SRS geometryBB = conv.convert(geometryBB); // add to instance bounding box if (bb == null) { bb = new BoundingBox(geometryBB); } else { bb.add(geometryBB); } } catch (Exception e) { log.error("Error converting instance bounding box to waypoint bounding box", e); // ignore geometry it.remove(); } } } } if (bb == null || geometries.isEmpty()) { // don't create way-point w/o geometries return null; } // use bounding box center as GEO position Point3D center = bb.getCenter(); GeoPosition pos = new GeoPosition(center.getX(), center.getY(), GenericWaypoint.COMMON_EPSG); // buffer bounding box if x or y dimension empty if (bb.getMinX() == bb.getMaxX()) { bb.setMinX(bb.getMinX() - BUFFER_VALUE); bb.setMaxX(bb.getMaxX() + BUFFER_VALUE); } if (bb.getMinY() == bb.getMaxY()) { bb.setMinY(bb.getMinY() - BUFFER_VALUE); bb.setMaxY(bb.getMaxY() + BUFFER_VALUE); } // set dummy z range (otherwise the RTree can't deal correctly with it) bb.setMinZ(-BUFFER_VALUE); bb.setMaxZ(BUFFER_VALUE); String name = findInstanceName(instance); // create the way-point // XXX in abstract method? InstanceWaypoint wp = new InstanceWaypoint(pos, bb, ref, geometries, instance.getDefinition(), name); // each way-point must have its own marker, as the marker stores the // marker areas wp.setMarker(createMarker(wp)); return wp; } /** * Determine the name for the given instance. * * @param instance the instance * @return the instance name or <code>null</code> if unknown */ private String findInstanceName(Instance instance) { for (String namePath : DEFAULT_NAME_PATHS) { Collection<Object> values = PropertyResolver.getValues(instance, namePath); if (values != null) { for (Object value : values) { if (value instanceof Instance) { value = ((Instance) value).getValue(); } if (value != null) { try { String name = ConversionUtil.getAs(value, String.class); if (name != null && !name.isEmpty()) { return name; } } catch (Exception e) { // ignore } } } } } // no name found return null; } /** * Create a new marker for a way-point. * * @param wp the way-point * @return the marker */ private Marker<? super InstanceWaypoint> createMarker(InstanceWaypoint wp) { // return new InstanceMarker(); return new StyledInstanceMarker(wp); } /** * @see AbstractTileOverlayPainter#getMaxOverlap() */ @Override protected int getMaxOverlap() { return 12; } /** * @return the instance service */ public InstanceService getInstanceService() { return instanceService; } /** * @return the data set */ public DataSet getDataSet() { return dataSet; } /** * Try to combine two selections. * * @param oldSelection the first selection * @param newSelection the second selection * @return the combined selection */ @SuppressWarnings("unchecked") public static ISelection combineSelection(ISelection oldSelection, ISelection newSelection) { if (newSelection == null) return oldSelection; else if (oldSelection == null) return newSelection; if (oldSelection instanceof InstanceSelection && newSelection instanceof InstanceSelection) { // combine scene selections Set<Object> values = new LinkedHashSet<Object>(); values.addAll(((InstanceSelection) oldSelection).toList()); values.addAll(((InstanceSelection) newSelection).toList()); return new DefaultInstanceSelection(new ArrayList<Object>(values)); } else { return newSelection; } } /** * Selects the preferred selection. * * @param oldSelection the first selection * @param newSelection the second selection * @return the preferred selection */ public static ISelection preferSelection(ISelection oldSelection, ISelection newSelection) { if (newSelection == null) return oldSelection; else if (oldSelection == null) return newSelection; // FIXME decide on which basis? // XXX for now, always return the new selection return newSelection; } /** * Get the selection for a given polygon on the screen. * * @param poly the polygon * @return a selection or <code>null</code> */ public ISelection getSelection(java.awt.Polygon poly) { Set<InstanceWaypoint> wps = findWaypoints(poly); return createSelection(wps); } /** * Get the selection for a given rectangle on the screen. * * @param rect the rectangle * @return a selection or <code>null</code> */ public ISelection getSelection(Rectangle rect) { Set<InstanceWaypoint> wps = findWaypoints(rect); return createSelection(wps); } private ISelection createSelection(Set<InstanceWaypoint> wps) { if (wps != null && !wps.isEmpty()) { List<InstanceReference> values = new ArrayList<InstanceReference>(wps.size()); for (InstanceWaypoint wp : wps) { values.add(wp.getValue()); } return new DefaultInstanceSelection(values); } return null; } /** * Get the selection for the given point. * * @param point the point (viewport coordinates) * @return a selection or <code>null</code> */ public ISelection getSelection(java.awt.Point point) { InstanceWaypoint wp = findWaypoint(point); if (wp != null) { return new DefaultInstanceSelection(wp.getValue()); } return null; } /** * @see ISelectionListener#selectionChanged(IWorkbenchPart, ISelection) */ @Override public void selectionChanged(IWorkbenchPart part, ISelection selection) { if (!(selection instanceof InstanceSelection)) { // only accept instance selections return; } // called when the selection has changed, to update the state of the // way-points Refresher refresh = prepareRefresh(false); refresh.setImageOp(new FastBlurFilter(2)); // collect instance references that are in the new selection Set<InstanceReference> selected = collectReferences(selection); Set<InstanceReference> toSelect = new HashSet<InstanceReference>(); toSelect.addAll(selected); toSelect.removeAll(lastSelected); // select all that previously have not been selected but are in the new // selection for (InstanceReference selRef : toSelect) { InstanceWaypoint wp = findWaypoint(selRef); if (wp != null) { wp.setSelected(true, refresh); } } // unselect all that have previously been selected but are not in the // new selection lastSelected.removeAll(selected); for (InstanceReference selRef : lastSelected) { InstanceWaypoint wp = findWaypoint(selRef); if (wp != null) { wp.setSelected(false, refresh); } } lastSelected = selected; refresh.execute(); } private Set<InstanceReference> collectReferences(ISelection selection) { Set<InstanceReference> selected = new HashSet<InstanceReference>(); if (selection instanceof IStructuredSelection) { for (Object element : ((IStructuredSelection) selection).toList()) { if (element instanceof InstanceReference) { selected.add((InstanceReference) element); } else if (element instanceof Instance) { Instance instance = (Instance) element; InstanceReference ref; try { ref = instanceService.getReference(instance);// , // dataSet); } catch (IllegalArgumentException iae) { // instance has no dataset set ref = new PseudoInstanceReference(instance); } if (ref != null) { selected.add(ref); } } } } return selected; } /** * @see GenericWaypointPainter#clearWaypoints() */ @Override public void clearWaypoints() { super.clearWaypoints(); lastSelected.clear(); } /** * @return the styleListener */ public StyleServiceListener getStyleListener() { return styleListener; } /** * @return the geometryListener */ public GeometrySchemaServiceListener getGeometryListener() { return geometryListener; } /** * @see ClipPainter#setClip(Clip) */ @Override public void setClip(Clip clip) { this.clip = clip; } /** * @see AbstractTileOverlayPainter#drawOverlay(Graphics2D, BufferedImage, * int, int, int, int, int, Rectangle, PixelConverter) */ @Override protected void drawOverlay(Graphics2D gfx, BufferedImage img, int zoom, int tilePosX, int tilePosY, int tileWidth, int tileHeight, Rectangle viewportBounds, PixelConverter converter) { if (clip == null) { super.drawOverlay(gfx, img, zoom, tilePosX, tilePosY, tileWidth, tileHeight, viewportBounds, converter); } else { Shape clipShape = clip.getClip(viewportBounds, tilePosX, tilePosY, tileWidth, tileHeight); if (clipShape != null) { // drawing allowed Shape orgClip = gfx.getClip(); gfx.clip(clipShape); super.drawOverlay(gfx, img, zoom, tilePosX, tilePosY, tileWidth, tileHeight, viewportBounds, converter); gfx.setClip(orgClip); } } } /** * @see GenericWaypointPainter#findWaypoint(Object) */ @Override public InstanceWaypoint findWaypoint(InstanceReference object) { return super.findWaypoint(object); } /** * @see InstanceServiceListener#transformationToggled(boolean) */ @Override public void transformationToggled(boolean enabled) { // ignore } /** * @see InstanceServiceListener#datasetAboutToChange(DataSet) */ @Override public void datasetAboutToChange(DataSet type) { // ignore } }