/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.core.gis.map;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.geotools.geometry.jts.JTS;
import com.opendoorlogistics.api.geometry.LatLongToScreen;
import com.opendoorlogistics.api.geometry.ODLGeom;
import com.opendoorlogistics.core.geometry.ODLGeomImpl;
import com.opendoorlogistics.core.geometry.Spatial;
import com.opendoorlogistics.core.gis.map.transforms.TransformGeomToWorldBitmap;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier;
/**
* Geometry transformed to world bitmap coord system
* @author Phil
*
*/
public final class OnscreenGeometry {
private Point2D.Double lineStringMidpoint;
private double linestringLength=Double.NaN;
private final boolean drawFilledBounds;
private final Geometry wbFinal;
public long getSizeInBytes(){
long ret=0;
if(wbFinal!=null){
ret += Spatial.getEstimatedSizeInBytes(wbFinal);
}
ret += 4*4 + 1 + 16 + 16;
return ret;
}
public static class CachedGeomKey{
private final ODLGeom geom;
private final Object otherKey;
public CachedGeomKey(ODLGeom geom, Object otherKey) {
this.geom = geom;
this.otherKey = otherKey;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((geom == null) ? 0 : geom.hashCode());
result = prime * result + ((otherKey == null) ? 0 : otherKey.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CachedGeomKey other = (CachedGeomKey) obj;
if (geom == null) {
if (other.geom != null)
return false;
} else if (!geom.equals(other.geom))
return false;
if (otherKey == null) {
if (other.otherKey != null)
return false;
} else if (!otherKey.equals(other.otherKey))
return false;
return true;
}
}
private static final boolean isAllLineStrings(Geometry g){
if(g==null){
return false;
}
if(LineString.class.isInstance(g)){
return true;
}
if(GeometryCollection.class.isInstance(g)){
int n = g.getNumGeometries();
for(int i =0 ; i<n;i++){
if(!isAllLineStrings(g.getGeometryN(i))){
return false;
}
}
}
return false;
}
public OnscreenGeometry(Geometry onscreen, boolean drawFilledBounds){
this.wbFinal = onscreen;
this.drawFilledBounds = drawFilledBounds;
}
/**
* Initialise the cached geometry and transform to screen coordinates
* so we can get the bounds. Simplifying the geometry is not done until later.
* @param geom
* @param simplify
* @param latLongToScreen
*/
public OnscreenGeometry(ODLGeomImpl geom, LatLongToScreen latLongToScreen) {
// geometry starts in long-lat; transform to world bitmap
Geometry initialGeometry = geom.getJTSGeometry();
try {
initialGeometry = JTS.transform(geom.getJTSGeometry(), new TransformGeomToWorldBitmap(latLongToScreen));
} catch (Throwable e) {
throw new RuntimeException(e);
}
// get bounds ensuring we have non-zero width and height in pixel space otherwise points don't draw...
Envelope bb = initialGeometry.getEnvelopeInternal();
Rectangle2D wbBounds = new Rectangle2D.Double(bb.getMinX(), bb.getMinY(),Math.max( bb.getWidth(),1),Math.max( bb.getHeight(),1));
double simpleTol = Spatial.getRendererSimplifyDistanceTolerance();
if(isAllLineStrings(initialGeometry)){
simpleTol = Spatial.getRendererSimplifyDistanceToleranceLineString();
}
if (simpleTol>0) {
// treat as circle if really small
if (wbBounds.getWidth() < simpleTol && wbBounds.getHeight() < simpleTol) {
wbFinal = createSimplificationCircle(wbBounds);
drawFilledBounds = true;
} else {
Geometry simplified = TopologyPreservingSimplifier.simplify(initialGeometry, simpleTol);
// if empty (because its too small), just treat as a point
if (simplified.getNumPoints() == 0) {
wbFinal = createSimplificationCircle(wbBounds);
drawFilledBounds = true;
} else {
wbFinal = simplified;
drawFilledBounds = false;
}
}
} else {
// use initial unsimplified geometry
drawFilledBounds = false;
wbFinal = initialGeometry;
}
}
private Geometry createSimplificationCircle(Rectangle2D wbBounds) {
Geometry geometry;
GeometryFactory factory = new GeometryFactory();
geometry = factory.createPoint(new Coordinate(wbBounds.getCenterX(), wbBounds.getCenterY()));
return geometry;
}
Geometry getJTSGeometry() {
return wbFinal;
}
/**
* If the image is really small (i.e. 1x2 pixels), rather than draw it we just draw a filled rectangle for its bounds
*
* @return
*/
boolean isDrawFilledBounds() {
return drawFilledBounds;
}
public boolean isLineString(){
Geometry geometry = getJTSGeometry();
if(geometry!=null){
return LineString.class.isInstance(geometry);
}
return false;
}
public synchronized double getLineStringLength(){
if(Double.isNaN(linestringLength)==false){
return linestringLength;
}
if(!isLineString()){
throw new RuntimeException("Cannot calculate the length of a line for a non-line geometry.");
}
LineString geometry = (LineString)getJTSGeometry();
linestringLength = geometry.getLength();
return linestringLength;
}
public synchronized Point2D getLineStringMidPoint(){
if(lineStringMidpoint!=null){
return lineStringMidpoint;
}
if(!isLineString()){
return null;
}
// check for empty geometry
LineString geometry = (LineString)getJTSGeometry();
double length = geometry.getLength();
if(length==0){
Point pnt = geometry.getCentroid();
lineStringMidpoint = new Point2D.Double(pnt.getX(), pnt.getY());
return lineStringMidpoint;
}
// Calculate for general case
Coordinate [] coords = geometry.getCoordinates();
double midpoint = length * 0.5;
double sum=0;
for(int i =0 ; i< coords.length-1 ; i++){
Coordinate a = coords[i];
Coordinate b = coords[i+1];
double segmentLength = a.distance(b);
if( (sum<=midpoint && (sum+segmentLength)>=midpoint) || (i==coords.length-2)){
double denominator = segmentLength>0 ? segmentLength:1;
Coordinate unitVector = new Coordinate( (b.x-a.x) / denominator, (b.y-a.y)/ denominator);
double lengthAlong = midpoint - sum;
lineStringMidpoint = new Point2D.Double(a.x + unitVector.x * lengthAlong, a.y + unitVector.y * lengthAlong);
break;
}
sum+=segmentLength;
}
return lineStringMidpoint;
}
}