/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.eas.client.controls.geopane;
import com.eas.client.controls.geopane.cache.MapTilesCache.RenderingTask;
import com.eas.client.controls.geopane.cache.*;
import com.eas.client.controls.geopane.cache.webtiles.AsyncWebMapTilesCache;
import com.eas.client.controls.geopane.events.*;
import com.vividsolutions.jts.geom.*;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.Layer;
import org.geotools.map.MapContent;
import org.geotools.map.event.MapLayerEvent;
import org.geotools.map.event.MapLayerListener;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.resources.BoundingBoxes;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
/**
*
* @author mg
*/
public class JGeoPane extends JTiledPane {
public static final double SCALE_MIN_BOUNDARY = 1.6e-5;
public static final double SCALE_MAX_BOUNDARY = 1e+3;
protected AffineTransform cartesian2Screen;
protected MapLayerListener generalChangesReflector;
protected MapLayerListener lightChangesReflector;
protected MapLayerListener screenChangesReflector;
protected Set<GeoPaneViewpointListener> viewListeners = new HashSet<>();
protected Set<GeoPaneMouseListener> mouseListeners = new HashSet<>();
protected MapContent screenContext;
protected GTRenderer screenRenderer = new StreamingRenderer();
/**
* Scales current view point transformation.
* @param sx Amount to scale by along the x axis.
* @param sy Amount to scale by along the y axis.
* @param isLast Flag, indicating that this scale is last in sequence of arbitrary view point operations.
* @throws Exception
*/
public void scaleView(double sx, double sy, boolean isLast) throws Exception {
recalcViewPoint();
double targetScaleX = cartesian2Screen.getScaleX() * sx;
double targetScaleY = cartesian2Screen.getScaleY() * sy;
if (targetScaleX >= SCALE_MIN_BOUNDARY && targetScaleX <= SCALE_MAX_BOUNDARY
&& targetScaleY >= SCALE_MIN_BOUNDARY && targetScaleY <= SCALE_MAX_BOUNDARY) {
cartesian2Screen.scale(sx, sy);
cache.scaleChanged();
lightweightCache.scaleChanged();
if (isLast) {
invalidate();
fireViewScaled(sx, sy, aoiToGeoGeometry(), aoiToCartesianGeometry());
}
}
}
/**
* Translates current view point transformation.
* @param tx Amount to translate by along the x axis.
* @param ty Amount to translate by along the y axis.
* @param isLast Flag, indicating that this translate is last in sequence of arbitrary view point operations.
* @throws Exception
*/
public void translateView(double tx, double ty, boolean isLast) throws Exception {
recalcViewPoint();
cartesian2Screen.translate(tx, ty);
clearCaches();
if (isLast) {
invalidate();
fireViewTranslated(tx, ty, aoiToGeoGeometry(), aoiToCartesianGeometry());
}
}
/**
* Translates current screen tiles grid to avoid actual view point transformation.
* Such grid translating leads to view point changing without any geo recalculations.
* This method also fires view point changed event if <code>isLast</code> param is true.
* @param x Amount of offset in screen coordinate space along horizontal axis.
* @param y Amount of offset in screen coordinate space along vertical axis.
* @param isLast Flag, indicating that this translate call is last call in some sequence.
* If true, view point change event will be fired.
* @throws Exception
*/
@Override
public void translateGrid(int x, int y, boolean isLast) throws Exception {
super.translateGrid(x, y, isLast);
if (isLast) {
Point2D.Double screenCenterBefore = screen2Cartesian(new Point(0, 0));
Point2D.Double screenCenterAfter = screen2Cartesian(new Point(x, y));
fireViewTranslated(screenCenterAfter.x - screenCenterBefore.x, screenCenterAfter.y - screenCenterBefore.y, aoiToGeoGeometry(), aoiToCartesianGeometry());
}
}
public String viewToString() {
return "View point tranform:\n" + cartesian2Screen.toString();
}
public String aoiToCartesianString() throws NoninvertibleTransformException {
Dimension size = getSize();
Point2D.Double lu = awtScreen2Cartesian(new Point(0, 0));
Point2D.Double br = awtScreen2Cartesian(new Point(size.width, size.height));
return "Area of interest in cartesian:\n" + "\tleft up corner: " + lu.toString() + "\n" + "\tright bottom corner" + br.toString();
}
public String aoiToGeoString() throws NoninvertibleTransformException, FactoryException, TransformException {
Dimension size = getSize();
Point2D.Double lu = awtScreen2Cartesian(new Point(0, 0));
lu = cartesian2Geo(lu);
Point2D.Double br = awtScreen2Cartesian(new Point(size.width, size.height));
br = cartesian2Geo(br);
return "Area of interest in lon/lat:\n" + "\tleft up corner: " + lu.toString() + "\n" + "\tright bottom corner" + br.toString();
}
public Geometry aoiToGeoGeometry() throws Exception {
Dimension size = getSize();
Point2D.Double lu = awtScreen2Cartesian(new Point(0, 0));
lu = cartesian2Geo(lu);
Point2D.Double br = awtScreen2Cartesian(new Point(size.width, size.height));
br = cartesian2Geo(br);
return constructRectyPolygonGeometry(lu, br);
}
public static Geometry constructRectyPolygonGeometry(Point2D.Double aLeftUpperCorner, Point2D.Double aBottomRightCorner) {
GeometryFactory gFactory = new GeometryFactory();
CoordinateSequenceFactory csFactory = gFactory.getCoordinateSequenceFactory();
Coordinate[] coordinates = new Coordinate[5];
coordinates[0] = new Coordinate(aLeftUpperCorner.x, aLeftUpperCorner.y);
coordinates[1] = new Coordinate(aBottomRightCorner.x, aLeftUpperCorner.y);
coordinates[2] = new Coordinate(aBottomRightCorner.x, aBottomRightCorner.y);
coordinates[3] = new Coordinate(aLeftUpperCorner.x, aBottomRightCorner.y);
coordinates[4] = new Coordinate(aLeftUpperCorner.x, aLeftUpperCorner.y);
CoordinateSequence cSeq = csFactory.create(coordinates);
return gFactory.createPolygon(new LinearRing(cSeq, gFactory), null);
}
public static Geometry createPointGeometry(Point2D.Double aPoint) {
GeometryFactory gFactory = new GeometryFactory();
Coordinate coordinate = new Coordinate(aPoint.x, aPoint.y);
return gFactory.createPoint(coordinate);
}
public Geometry aoiToCartesianGeometry() throws Exception {
Dimension size = getSize();
Point2D.Double lu = awtScreen2Cartesian(new Point(0, 0));
Point2D.Double br = awtScreen2Cartesian(new Point(size.width, size.height));
return constructRectyPolygonGeometry(lu, br);
}
public double snapScale(double coef) {
if (getBackingUrl() != null && !getBackingUrl().isEmpty()) {
double powValue = Math.log(coef) / Math.log(2);
Long lPowValue = Math.round(powValue);
return Math.pow(2, lPowValue);
} else {
return coef;
}
}
protected class MapLayerListenerAdapter implements MapLayerListener {
@Override
public void layerChanged(MapLayerEvent event) {
}
@Override
public void layerShown(MapLayerEvent event) {
}
@Override
public void layerHidden(MapLayerEvent event) {
}
@Override
public void layerSelected(MapLayerEvent event) {
}
@Override
public void layerDeselected(MapLayerEvent event) {
}
@Override
public void layerPreDispose(MapLayerEvent mle) {
throw new UnsupportedOperationException("Not supported yet.");
}
}
protected class LayersChangesReflector extends MapLayerListenerAdapter {
protected TilesCache layersCache;
public LayersChangesReflector(TilesCache aCache) {
super();
layersCache = aCache;
}
@Override
public void layerChanged(MapLayerEvent event) {
layersCache.clear();
repaint();
}
}
protected class ScreenChangesReflector extends MapLayerListenerAdapter {
@Override
public void layerChanged(MapLayerEvent event) {
repaint();
}
}
protected class TileRenderedRepainter implements RenderingTaskListener {
@Override
public void taskCompleted(RenderingTask aTask) {
repaint();
}
}
@Override
public synchronized void repaint() {
super.repaint();
}
public JGeoPane(MapContent aContext, MapContent aLightweightContext, boolean aAsync) {
this(aContext, aLightweightContext, null, aAsync);
}
public JGeoPane(MapContent aContext, MapContent aLightweightContext, MapContent aScreenContext, boolean aAsync) {
super();
setLayout(new GeoLayout());
screenContext = aScreenContext;
cartesian2Screen = new AffineTransform();
if (aAsync) {
cacheLock = new ReentrantReadWriteLock();
lightweightCacheLock = new ReentrantReadWriteLock();
cache = new AsyncMapTilesCache(aContext, cacheLock, cartesian2Screen);
lightweightCache = new AsyncMapTilesCache(aLightweightContext, lightweightCacheLock, cartesian2Screen);
((AsyncMapTilesCache) cache).addRenderingTaskListener(new TileRenderedRepainter());
if (lightweightCache instanceof AsyncMapTilesCache) {
((AsyncMapTilesCache) lightweightCache).addRenderingTaskListener(new TileRenderedRepainter());
}
} else {
cache = new SyncMapTilesCache(aContext, cartesian2Screen);
lightweightCache = new SyncMapTilesCache(aLightweightContext, cartesian2Screen);
}
cache.setBackground(getBackground());
lightweightCache.setImageType(BufferedImage.TYPE_INT_ARGB);
addLayersListeners();
}
public JGeoPane(MapContent aContext, MapContent aLightweightContext, String aBackingUrl) {
this(aContext, aLightweightContext, null, aBackingUrl);
}
public JGeoPane(MapContent aContext, MapContent aLightweightContext, MapContent aScreenContext, String aBackingUrl) {
super();
setLayout(new GeoLayout());
screenContext = aScreenContext;
cartesian2Screen = new AffineTransform();
cacheLock = new ReentrantReadWriteLock();
lightweightCacheLock = new ReentrantReadWriteLock();
if (aBackingUrl != null && !aBackingUrl.isEmpty()) {
cache = GeoPaneUtils.createWebTilesCache(aContext, cacheLock, cartesian2Screen, aBackingUrl);
lightweightCache = new AsyncMapTilesCache(aLightweightContext, lightweightCacheLock, cartesian2Screen);
} else {
cache = new AsyncMapTilesCache(aContext, cacheLock, cartesian2Screen);
lightweightCache = new AsyncMapTilesCache(aLightweightContext, lightweightCacheLock, cartesian2Screen);
}
assert cache instanceof AsyncMapTilesCache;
((AsyncMapTilesCache) cache).addRenderingTaskListener(new TileRenderedRepainter());
if (lightweightCache instanceof AsyncMapTilesCache) {
((AsyncMapTilesCache) lightweightCache).addRenderingTaskListener(new TileRenderedRepainter());
}
cache.setBackground(getBackground());
lightweightCache.setImageType(BufferedImage.TYPE_INT_ARGB);
addLayersListeners();
}
public MapContent getScreenContext() {
return screenContext;
}
public void setScreenContext(MapContent aValue) {
screenContext = aValue;
}
private void removeLayersListeners() {
MapContent generalContext = ((MapTilesCache) cache).getMapDisplayContext();
MapContent lightweightContext = ((MapTilesCache) lightweightCache).getMapDisplayContext();
if (generalChangesReflector != null) {
for (Layer layer : generalContext.layers()) {
layer.removeMapLayerListener(generalChangesReflector);
}
}
if (lightChangesReflector != null) {
for (Layer layer : lightweightContext.layers()) {
layer.removeMapLayerListener(lightChangesReflector);
}
}
if (screenContext != null && screenChangesReflector != null) {
for (Layer layer : screenContext.layers()) {
layer.removeMapLayerListener(screenChangesReflector);
}
}
}
private void addLayersListeners() {
MapContent generalContext = ((MapTilesCache) cache).getMapDisplayContext();
MapContent lightweightContext = ((MapTilesCache) lightweightCache).getMapDisplayContext();
generalChangesReflector = new LayersChangesReflector(cache);
lightChangesReflector = new LayersChangesReflector(lightweightCache);
screenChangesReflector = new ScreenChangesReflector();
for (Layer layer : generalContext.layers()) {
layer.addMapLayerListener(generalChangesReflector);
}
for (Layer layer : lightweightContext.layers()) {
layer.addMapLayerListener(lightChangesReflector);
}
if (screenContext != null) {
for (Layer layer : screenContext.layers()) {
layer.addMapLayerListener(screenChangesReflector);
}
}
}
public MapLayerListener getGeneralChangesReflector() {
return generalChangesReflector;
}
public MapLayerListener getLightChangesReflector() {
return lightChangesReflector;
}
@Override
protected void paintScreenContext(Graphics2D g) {
if (screenContext != null) {
synchronized (screenContext) {
try {
// let's paint selection and phanton layers
screenRenderer.setMapContent(screenContext);
Rectangle clip = g.getClipBounds();
Point2D.Double lu = screen2Cartesian(clip.getLocation());
Point2D.Double br = screen2Cartesian(new Point(clip.x + clip.width, clip.y + clip.height));
Geometry aoiGeometry = constructRectyPolygonGeometry(lu, br);
Envelope aoiEnvelope = new ReferencedEnvelope(aoiGeometry.getEnvelopeInternal(), screenContext.getCoordinateReferenceSystem());
screenRenderer.paint(g, clip, aoiEnvelope, cartesian2Screen);
} catch (NoninvertibleTransformException ex) {
Logger.getLogger(JGeoPane.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
super.paintScreenContext(g);
}
public MapContent getGeneralMapContext() {
assert cache instanceof MapTilesCache : "JGeoPane instance requires MapTilesCache instance as general cache";
return ((MapTilesCache) cache).getMapDisplayContext();
}
public MapContent getLightweightMapContext() {
assert lightweightCache instanceof MapTilesCache : "JGeoPane instance requires MapTilesCache instance as general cache";
return ((MapTilesCache) lightweightCache).getMapDisplayContext();
}
public String getBackingUrl() {
if (cache instanceof AsyncWebMapTilesCache) {
return ((AsyncWebMapTilesCache) cache).getBackingUrl();
} else {
return null;
}
}
public void setBackingUrl(String aBackingUrl) {
String oldBackingUrl = getBackingUrl();
if (oldBackingUrl == null) {
oldBackingUrl = "";
}
if (aBackingUrl == null) {
aBackingUrl = "";
}
if (!oldBackingUrl.equals(aBackingUrl)) {
removeLayersListeners();
MapContent generalMapContext = getGeneralMapContext();
assert cache instanceof AsyncMapTilesCache;
((AsyncMapTilesCache) cache).shutdown();
if (aBackingUrl.isEmpty()) {
cache = new AsyncMapTilesCache(generalMapContext, cacheLock, cartesian2Screen);
} else {
cache = GeoPaneUtils.createWebTilesCache(generalMapContext, cacheLock, cartesian2Screen, aBackingUrl);
}
cache.setBackground(getBackground());
((AsyncMapTilesCache) cache).addRenderingTaskListener(new TileRenderedRepainter());
cache.scaleChanged();
lightweightCache.scaleChanged();
addLayersListeners();
}
repaint();
}
public void fit() throws Exception {
MapContent generalMapContext = getGeneralMapContext();
Dimension size = getSize();
Envelope projectedBounds = generalMapContext.getMaxBounds();
double destWidth = projectedBounds.getWidth();
double destHeight = projectedBounds.getHeight();
Coordinate centre = projectedBounds.centre();
Point2D.Double screenLT = awtScreen2Cartesian(new Point(0, 0));
Point2D.Double screenBR = awtScreen2Cartesian(new Point(size.width, size.height));
double srcWidth = screenBR.x - screenLT.x;
double srcHeight = screenBR.y - screenLT.y;
double sx = srcWidth / destWidth;
double sy = srcHeight / destHeight;
double coef = Math.min(sx, sy);
coef = snapScale(coef);
scaleView(coef, coef, false);
Point2D.Double projectedScreenCenter = screen2Cartesian(new Point(0, 0));
translateView(projectedScreenCenter.x - centre.x, projectedScreenCenter.y - centre.y, true);
repaint();
}
/**
* Fits screen to specified geometry bounds.
* @param aArea A geometry in geo coordinates space.
* @throws Exception
*/
public void fit(Geometry aArea) throws Exception {
Geometry bounds = aArea.getBoundary();
Envelope envBounds = bounds.getEnvelopeInternal();
Point2D.Double leftUpCorner = new Point2D.Double(envBounds.getMinX(), envBounds.getMinY());
Point2D.Double rightBottomCorner = new Point2D.Double(envBounds.getMaxX(), envBounds.getMaxY());
Point2D.Double cartlu = geo2Cartesian(leftUpCorner);
Point2D.Double cartrb = geo2Cartesian(rightBottomCorner);
double destWidth = Math.abs(cartrb.getX() - cartlu.getX());
double destHeight = Math.abs(cartrb.getY() - cartlu.getY());
Coordinate centre = new Coordinate((cartrb.getX() + cartlu.getX()) / 2, (cartrb.getY() + cartlu.getY()) / 2);
Dimension size = getSize();
Point2D.Double screenLT = awtScreen2Cartesian(new Point(0, 0));
Point2D.Double screenBR = awtScreen2Cartesian(new Point(size.width, size.height));
double srcWidth = screenBR.x - screenLT.x;
double srcHeight = screenBR.y - screenLT.y;
double sx = srcWidth / destWidth;
double sy = srcHeight / destHeight;
double coef = Math.min(sx, sy);
coef = snapScale(coef);
scaleView(coef, coef, false);
Point2D.Double projectedScreenCenter = screen2Cartesian(new Point(0, 0));
translateView(projectedScreenCenter.x - centre.x, projectedScreenCenter.y - centre.y, true);
repaint();
}
public Point.Double getGeoPosition() throws Exception {
return cartesian2Geo(screen2Cartesian(new Point(0, 0)));
}
public void goToGeoPosition(Point.Double aGeoPosition) throws Exception {
Point2D.Double projectedScreenCenter = screen2Cartesian(new Point(0, 0));
Point2D.Double projectedTarget = geo2Cartesian(aGeoPosition);
translateView(projectedScreenCenter.x - projectedTarget.x, projectedScreenCenter.y - projectedTarget.y, true);
repaint();
}
/**
* Ajusts view point according to tiles grid offset.
* E.g. converts grid offset to view point offset.
* Clears grid offset. Doesn't clear any caches or repaint.
* @throws NoninvertibleTransformException
*/
protected void recalcViewPoint() throws NoninvertibleTransformException {
Point2D.Double screenCenterOnCartesian = screen2Cartesian(new Point(0, 0));
Point2D.Double screenViewpointOnCartesian = screen2Cartesian(new Point(-offset.x, -offset.y));
cartesian2Screen.translate(screenViewpointOnCartesian.getX() - screenCenterOnCartesian.getX(), screenViewpointOnCartesian.getY() - screenCenterOnCartesian.getY());
clearGridTranslating();
}
public void clearCaches() {
cache.clear();
lightweightCache.clear();
}
public void clearLightweightCache() {
lightweightCache.clear();
}
public Point2D.Double cartesian2Geo(Point2D.Double aPt) throws FactoryException, TransformException {
MathTransform2D transform = ((MapTilesCache) cache).getCartesian2MapTransform();
Point2D.Double res = new Point2D.Double();
return (Point2D.Double) transform.transform(aPt, res);
}
public Point2D.Double geo2Cartesian(Point2D.Double aPt) throws FactoryException, TransformException {
MathTransform2D transform = ((MapTilesCache) cache).getMap2CartesianTransform();
Point2D.Double res = new Point2D.Double();
return (Point2D.Double) transform.transform(aPt, res);
}
public Point cartesian2Screen(Point2D.Double aPt) throws FactoryException, TransformException {
Point2D.Double screenPt = new Point2D.Double();
cartesian2Screen.transform(aPt, screenPt);
return new Point((int) Math.round(screenPt.getX()), (int) Math.round(screenPt.getY()));
}
/**
* Converts screen point to cartesian space. Takes into account inner tiles grid offset.
* @param aScreenPoint Point, expressed in AWT events coordinate space. E.g. center of the control will have coordinates (size.width/2, size.height/2).
* @return Point in cartesian space.
* @throws NoninvertibleTransformException
*/
public Point2D.Double awtScreen2Cartesian(Point aScreenPoint) throws NoninvertibleTransformException {
assert cartesian2Screen != null;
Dimension size = getSize();
Point2D.Double centeredScreenPt = new Point2D.Double(aScreenPoint.x - size.width / 2 + offset.x, aScreenPoint.y - size.height / 2 + offset.y);
Point2D.Double cartesianPt = new Point2D.Double();
return (Point2D.Double) cartesian2Screen.inverseTransform(centeredScreenPt, cartesianPt);
}
/**
* Converts screen point to cartesian space. Takes into account inner tiles grid offset.
* @param aScreenPoint Point, expressed in centered screen coordinate space. E.g. center of the control will have coordinates (0, 0).
* @return Point in cartesian space.
* @throws NoninvertibleTransformException
*/
public Point2D.Double screen2Cartesian(Point aScreenPoint) throws NoninvertibleTransformException {
assert cartesian2Screen != null;
Point2D.Double centeredScreenPt = new Point2D.Double(aScreenPoint.x + offset.x, aScreenPoint.y + offset.y);
Point2D.Double cartesianPt = new Point2D.Double();
return (Point2D.Double) cartesian2Screen.inverseTransform(centeredScreenPt, cartesianPt);
}
protected void fireViewTranslated(double aDx, double aDy, Geometry aNewGeoAreaOfInterest, Geometry aNewCartesianAreaOfInterest) throws Exception {
ViewpointTranslatedEvent event = new ViewpointTranslatedEvent(cartesian2Screen, aDx, aDy, aNewGeoAreaOfInterest, aNewCartesianAreaOfInterest);
for (GeoPaneViewpointListener l : viewListeners) {
try {
l.translated(event);
} catch (Exception ex) {
Logger.getLogger(JGeoPane.class.getName()).log(Level.SEVERE, "while firing an event: \"viewTranslatedEvent\" on listener: " + l.toString(), ex);
}
}
}
protected void fireViewScaled(double aScaleX, double aScaleY, Geometry aNewGeoAreaOfInterest, Geometry aNewCartesianAreaOfInterest) throws Exception {
ViewpointScaledEvent event = new ViewpointScaledEvent(cartesian2Screen, aScaleX, aScaleY, aNewGeoAreaOfInterest, aNewCartesianAreaOfInterest);
for (GeoPaneViewpointListener l : viewListeners) {
try {
l.scaled(event);
} catch (Exception ex) {
Logger.getLogger(JGeoPane.class.getName()).log(Level.SEVERE, "while firing an event: \"viewScaledEvent\" on listener: " + l.toString(), ex);
}
}
}
public void fireMapClicked(MouseEvent awtEvent, Point2D.Double cartesianPoint, Point2D.Double geoPoint) {
MapClickedEvent event = new MapClickedEvent(awtEvent, createPointGeometry(cartesianPoint), createPointGeometry(geoPoint));
for (GeoPaneMouseListener l : mouseListeners) {
try {
l.clicked(event);
} catch (Exception ex) {
Logger.getLogger(JGeoPane.class.getName()).log(Level.SEVERE, "while firing an event: \"mapClickedEvent\" on listener: " + l.toString(), ex);
}
}
}
public void addGeoPaneViewpointListener(GeoPaneViewpointListener l) {
viewListeners.add(l);
}
public boolean removeGeoPaneViewpointListener(GeoPaneViewpointListener l) {
return viewListeners.remove(l);
}
public void addGeoPaneMouseListener(GeoPaneMouseListener l) {
mouseListeners.add(l);
}
public boolean removeGeoPaneMouseListener(GeoPaneMouseListener l) {
return mouseListeners.remove(l);
}
}