/******************************************************************************* * Copyright 2012 Geoscience Australia * * 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 au.gov.ga.earthsci.worldwind.common.layers.borehole; import gov.nasa.worldwind.WorldWind; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.avlist.AVList; import gov.nasa.worldwind.event.SelectEvent; import gov.nasa.worldwind.event.SelectListener; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.layers.AbstractLayer; import gov.nasa.worldwind.pick.PickSupport; import gov.nasa.worldwind.pick.PickedObject; import gov.nasa.worldwind.render.AnnotationRenderer; import gov.nasa.worldwind.render.BasicAnnotationRenderer; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.render.GlobeAnnotation; import gov.nasa.worldwind.render.markers.BasicMarkerAttributes; import gov.nasa.worldwind.render.markers.Marker; import gov.nasa.worldwind.render.markers.MarkerAttributes; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.media.opengl.GL2; import org.gdal.osr.CoordinateTransformation; import au.gov.ga.earthsci.worldwind.common.WorldWindowRegistry; import au.gov.ga.earthsci.worldwind.common.layers.Bounds; import au.gov.ga.earthsci.worldwind.common.layers.point.types.MarkerPointLayer; import au.gov.ga.earthsci.worldwind.common.layers.styled.Attribute; import au.gov.ga.earthsci.worldwind.common.layers.styled.BasicStyleProvider; import au.gov.ga.earthsci.worldwind.common.layers.styled.Style; import au.gov.ga.earthsci.worldwind.common.layers.styled.StyleAndText; import au.gov.ga.earthsci.worldwind.common.layers.styled.StyleProvider; import au.gov.ga.earthsci.worldwind.common.render.DeepPickingMarkerRenderer; import au.gov.ga.earthsci.worldwind.common.render.fastshape.FastShape; import au.gov.ga.earthsci.worldwind.common.util.AVKeyMore; import au.gov.ga.earthsci.worldwind.common.util.ColorMap; import au.gov.ga.earthsci.worldwind.common.util.CoordinateTransformationUtil; import au.gov.ga.earthsci.worldwind.common.util.DefaultLauncher; import au.gov.ga.earthsci.worldwind.common.util.OnTopGlobeAnnotation; import au.gov.ga.earthsci.worldwind.common.util.Util; import au.gov.ga.earthsci.worldwind.common.util.Validate; /** * Basic implementation of the {@link BoreholeLayer}. Draws markers for each * borehole location, and coloured lines for borehole samples. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class BasicBoreholeLayer extends AbstractLayer implements BoreholeLayer, SelectListener { private static final Color DEFAULT_SAMPLE_COLOR = Color.GRAY; protected BoreholeProvider boreholeProvider; protected StyleProvider boreholeStyleProvider = new BasicStyleProvider(); protected StyleProvider sampleStyleProvider = new BasicStyleProvider(); protected final List<Borehole> boreholes = new ArrayList<Borehole>(); protected final Map<Object, BoreholeImpl> idToBorehole = new HashMap<Object, BoreholeImpl>(); protected final DeepPickingMarkerRenderer markerRenderer = new DeepPickingMarkerRenderer(); protected final AnnotationRenderer annotationRenderer = new BasicAnnotationRenderer(); protected URL context; protected String url; protected String dataCacheName; protected String uniqueIdentifierAttribute; protected String sampleDepthFromAttribute; protected String sampleDepthToAttribute; protected boolean attributesRepresentPositiveDepth = true; protected double lineWidth = 5; protected Double minimumDistance; protected CoordinateTransformation coordinateTransformation; protected ColorMap colorMap; protected GlobeAnnotation tooltipAnnotation; protected FastShape pathShape; protected FastShape samplesShape; protected float[] pickingColorBuffer; protected final PickSupport pickSupport = new PickSupport(); @SuppressWarnings("unchecked") public BasicBoreholeLayer(AVList params) { context = (URL) params.getValue(AVKeyMore.CONTEXT_URL); url = params.getStringValue(AVKey.URL); dataCacheName = params.getStringValue(AVKey.DATA_CACHE_NAME); boreholeProvider = (BoreholeProvider) params.getValue(AVKeyMore.DATA_LAYER_PROVIDER); boreholeStyleProvider.setStyles((List<Style>) params.getValue(AVKeyMore.DATA_LAYER_STYLES)); boreholeStyleProvider.setAttributes((List<Attribute>) params.getValue(AVKeyMore.DATA_LAYER_ATTRIBUTES)); sampleStyleProvider.setStyles((List<Style>) params.getValue(AVKeyMore.BOREHOLE_SAMPLE_STYLES)); sampleStyleProvider.setAttributes((List<Attribute>) params.getValue(AVKeyMore.BOREHOLE_SAMPLE_ATTRIBUTES)); uniqueIdentifierAttribute = params.getStringValue(AVKeyMore.BOREHOLE_UNIQUE_IDENTIFIER_ATTRIBUTE); sampleDepthFromAttribute = params.getStringValue(AVKeyMore.BOREHOLE_SAMPLE_DEPTH_FROM_ATTRIBUTE); sampleDepthToAttribute = params.getStringValue(AVKeyMore.BOREHOLE_SAMPLE_DEPTH_TO_ATTRIBUTE); Boolean b = (Boolean) params.getValue(AVKeyMore.BOREHOLE_SAMPLE_DEPTH_ATTRIBUTES_POSITIVE); if (b != null) { attributesRepresentPositiveDepth = b; } Double d = (Double) params.getValue(AVKeyMore.LINE_WIDTH); if (d != null) { lineWidth = d; } String s = (String) params.getValue(AVKey.COORDINATE_SYSTEM); if (s != null) { setCoordinateTransformation(CoordinateTransformationUtil.getTransformationToWGS84(s)); } ColorMap cm = (ColorMap) params.getValue(AVKeyMore.COLOR_MAP); if (cm != null) { setColorMap(cm); } minimumDistance = (Double) params.getValue(AVKeyMore.MINIMUM_DISTANCE); Validate.notBlank(url, "Borehole data url not set"); Validate.notBlank(dataCacheName, "Borehole data cache name not set"); Validate.notNull(boreholeProvider, "Borehole data provider is null"); Validate.notNull(boreholeStyleProvider.getStyles(), "Borehole style list is null"); Validate.notNull(boreholeStyleProvider.getAttributes(), "Borehole attribute list is null"); Validate.notNull(sampleStyleProvider.getStyles(), "Borehole sample style list is null"); Validate.notNull(sampleStyleProvider.getAttributes(), "Borehole sample attribute list is null"); // Init tooltip annotation this.tooltipAnnotation = new OnTopGlobeAnnotation("", Position.fromDegrees(0, 0, 0)); Font font = Font.decode("Arial-Plain-15"); this.tooltipAnnotation.getAttributes().setFont(font); this.tooltipAnnotation.getAttributes().setSize(new Dimension(270, 0)); this.tooltipAnnotation.getAttributes().setDistanceMinScale(1); this.tooltipAnnotation.getAttributes().setDistanceMaxScale(1); this.tooltipAnnotation.getAttributes().setVisible(false); this.tooltipAnnotation.setPickEnabled(false); this.tooltipAnnotation.setAlwaysOnTop(true); this.tooltipAnnotation .setAltitudeMode(isFollowTerrain() ? WorldWind.RELATIVE_TO_GROUND : WorldWind.ABSOLUTE); markerRenderer.setKeepSeparated(false); markerRenderer.setDrawImmediately(true); if (isFollowTerrain()) { markerRenderer.setOverrideMarkerElevation(true); markerRenderer.setElevation(0); } WorldWindowRegistry.INSTANCE.addSelectListener(this); } @Override public Bounds getBounds() { return boreholeProvider.getBounds(); } @Override public boolean isFollowTerrain() { return boreholeProvider.isFollowTerrain(); } @Override public URL getUrl() throws MalformedURLException { return new URL(context, url); } @Override public String getDataCacheName() { return dataCacheName; } @Override public void addBorehole(Borehole borehole) { synchronized (boreholes) { boreholes.add(borehole); } } @Override public void addBoreholeSample(Position position, AVList attributeValues) { Object id = attributeValues.getValue(uniqueIdentifierAttribute); Validate.notNull(id, "Borehole attributes do not contain an identifier"); Double depthFrom = Util.objectToDouble(attributeValues.getValue(sampleDepthFromAttribute)); Double depthTo = Util.objectToDouble(attributeValues.getValue(sampleDepthToAttribute)); Validate.notNull(depthFrom, "Borehole sample attributes do not contain a valid depth-from"); Validate.notNull(depthTo, "Borehole sample attributes do not contain a valid depth-to"); BoreholeImpl borehole = idToBorehole.get(id); if (borehole == null) { MarkerAttributes markerAttributes = new BasicMarkerAttributes(); borehole = new BoreholeImpl(position, markerAttributes); idToBorehole.put(id, borehole); synchronized (boreholes) { boreholes.add(borehole); } StyleAndText boreholeProperties = boreholeStyleProvider.getStyle(attributeValues); borehole.setUrl(boreholeProperties.link); borehole.setTooltipText(boreholeProperties.text); boreholeProperties.style.setPropertiesFromAttributes(context, attributeValues, markerAttributes, borehole); MarkerPointLayer.fixShapeType(markerAttributes); } BoreholeSampleImpl sample = new BoreholeSampleImpl(borehole); borehole.addSample(sample); sample.setDepthFrom(attributesRepresentPositiveDepth ? depthFrom : -depthFrom); sample.setDepthTo(attributesRepresentPositiveDepth ? depthTo : -depthTo); StyleAndText sampleProperties = sampleStyleProvider.getStyle(attributeValues); sample.setText(sampleProperties.text); sample.setLink(sampleProperties.link); sampleProperties.style.setPropertiesFromAttributes(context, attributeValues, sample); } @Override public void loadComplete() { List<Position> positions = new ArrayList<Position>(); List<Color> colors = new ArrayList<Color>(); List<Position> pathPositions = new ArrayList<Position>(); for (Borehole borehole : boreholes) { borehole.loadComplete(); BoreholePath path = borehole.getPath(); for (BoreholeSample sample : borehole.getSamples()) { Position sampleTop = path.getPosition(sample.getDepthFrom()); Position sampleBottom = path.getPosition(sample.getDepthTo()); positions.add(sampleTop); positions.add(sampleBottom); Color sampleColor = sample.getColor(); sampleColor = sampleColor != null ? sampleColor : getDefaultSampleColor(); colors.add(sampleColor); colors.add(sampleColor); } Position lastPosition = null; for (Position position : path.getPositions().values()) { if (lastPosition != null) { pathPositions.add(lastPosition); pathPositions.add(position); } lastPosition = position; } } FastShape pathShape = new FastShape(pathPositions, GL2.GL_LINES); pathShape.setColor(Color.LIGHT_GRAY); pathShape.setLineWidth(1.0); pathShape.setFollowTerrain(isFollowTerrain()); float[] boreholeColorBuffer = FastShape.color3ToFloats(colors); pickingColorBuffer = new float[colors.size() * 3]; FastShape samplesShape = new FastShape(positions, GL2.GL_LINES); samplesShape.setColorBuffer(boreholeColorBuffer); samplesShape.setFollowTerrain(isFollowTerrain()); samplesShape.setLineWidth(lineWidth); //set at the end, so that half-setup shape isn't rendered on the render thread this.pathShape = pathShape; this.samplesShape = samplesShape; } @Override public Double getMinimumDistance() { return minimumDistance; } @Override protected void doPick(DrawContext dc, Point point) { doRender(dc); } @Override protected void doRender(DrawContext dc) { if (!isEnabled()) { return; } boreholeProvider.requestData(this); synchronized (boreholes) { markerRenderer.render(dc, allMarkers()); annotationRenderer.render(dc, tooltipAnnotation, tooltipAnnotation.getAnnotationDrawPoint(dc), this); if (samplesShape != null) { if (!dc.isPickingMode()) { samplesShape.render(dc); pathShape.render(dc); } else { boolean oldDeepPicking = dc.isDeepPickingEnabled(); try { //deep picking needs to be enabled, because boreholes are below the surface dc.setDeepPickingEnabled(true); pickSupport.beginPicking(dc); //First pick on the entire object by setting the shape to a single color. //This will determine if we have to go further and pick individual samples. Color overallPickColor = dc.getUniquePickColor(); pickSupport.addPickableObject(overallPickColor.getRGB(), samplesShape, getBounds().center); samplesShape.setColor(overallPickColor); samplesShape.setColorBufferEnabled(false); samplesShape.render(dc); samplesShape.setColorBufferEnabled(true); PickedObject object = pickSupport.getTopObject(dc, dc.getPickPoint()); pickSupport.clearPickList(); if (object != null && object.getObject() == samplesShape) { //This layer has been picked; now try picking the samples individually //Put unique pick colours into the pickingColorBuffer (2 per sample) int i = 0; for (Borehole borehole : boreholes) { for (BoreholeSample sample : borehole.getSamples()) { Color color = dc.getUniquePickColor(); pickSupport.addPickableObject(color.getRGB(), sample, getBounds().center); for (int j = 0; j < 2; j++) { pickingColorBuffer[i++] = color.getRed() / 255f; pickingColorBuffer[i++] = color.getGreen() / 255f; pickingColorBuffer[i++] = color.getBlue() / 255f; } } } //render the shape with the pickingColorBuffer, and then resolve the pick samplesShape.setPickingColorBuffer(pickingColorBuffer); samplesShape.render(dc); pickSupport.resolvePick(dc, dc.getPickPoint(), this); } } finally { pickSupport.endPicking(dc); dc.setDeepPickingEnabled(oldDeepPicking); } } } } } @Override public boolean isLoading() { return boreholeProvider.isLoading(); } @Override public void addLoadingListener(LoadingListener listener) { boreholeProvider.addLoadingListener(listener); } @Override public void removeLoadingListener(LoadingListener listener) { boreholeProvider.removeLoadingListener(listener); } @Override public void selected(SelectEvent e) { if (e == null) { return; } PickedObject topPickedObject = e.getTopPickedObject(); Object object = topPickedObject != null ? topPickedObject.getObject() : null; if (object instanceof Borehole || object instanceof BoreholeSample || object instanceof BoreholeMarker) { highlight(object, true); if (e.getEventAction() == SelectEvent.LEFT_CLICK) { String link = null; if (object instanceof Borehole) { link = ((Borehole) object).getLink(); } else if (object instanceof BoreholeSample) { link = ((BoreholeSample) object).getLink(); } else if (object instanceof BoreholeMarker) { link = ((BoreholeMarker) object).getLink(); } if (link != null) { try { URL url = new URL(link); DefaultLauncher.openURL(url); } catch (MalformedURLException m) { } } } } else { highlight(null, false); } } @Override public Color getDefaultSampleColor() { return DEFAULT_SAMPLE_COLOR; } @Override public CoordinateTransformation getCoordinateTransformation() { return coordinateTransformation; } public void setCoordinateTransformation(CoordinateTransformation coordinateTransformation) { this.coordinateTransformation = coordinateTransformation; } @Override public ColorMap getColorMap() { return colorMap; } public void setColorMap(ColorMap colorMap) { this.colorMap = colorMap; } /** * Highlight the provided object by showing the tooltip annotation over it. * * @param object * {@link Borehole} or {@link BoreholeSample} to highlight * @param highlight * True to show tooltip annotation, false to hide it */ protected void highlight(Object object, boolean highlight) { if (highlight) { String text = null; Position position = null; if (object instanceof Borehole) { Borehole borehole = (Borehole) object; text = borehole.getText(); position = borehole.getPosition(); } else if (object instanceof BoreholeSample) { BoreholeSample sample = (BoreholeSample) object; text = sample.getText(); double depth = (sample.getDepthTo() + sample.getDepthFrom()) / 2d; position = sample.getBorehole().getPath().getPosition(depth); } else if (object instanceof BoreholeMarker) { BoreholeMarker marker = (BoreholeMarker) object; text = marker.getText(); position = marker.getBorehole().getPath().getPosition(marker.getDepth()); } if (text != null) { tooltipAnnotation.setText(text); tooltipAnnotation.setPosition(position); tooltipAnnotation.getAttributes().setVisible(true); } } else { tooltipAnnotation.getAttributes().setVisible(false); } } protected Iterable<Marker> allMarkers() { return new Iterable<Marker>() { @Override public Iterator<Marker> iterator() { return new Iterator<Marker>() { private Iterator<? extends Marker> current = boreholes.iterator(); private int boreholeIndex = 0; @Override public boolean hasNext() { if (current.hasNext()) { return true; } while (boreholeIndex < boreholes.size()) { current = boreholes.get(boreholeIndex++).getMarkers().iterator(); if (current.hasNext()) { return true; } } return false; } @Override public Marker next() { if (!hasNext()) { return null; } return current.next(); } }; } }; } }