/*
* Copyright (C) 2014 Alec Dhuse
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package co.foldingmap.map.vector;
import co.foldingmap.map.MapView;
import co.foldingmap.map.MapObject;
import co.foldingmap.map.Visibility;
import co.foldingmap.map.ObjectNotWithinBoundsException;
import co.foldingmap.map.Layer;
import co.foldingmap.Logger;
import co.foldingmap.dataStructures.PropertyValuePair;
import co.foldingmap.map.themes.ColorStyle;
import co.foldingmap.map.themes.MapTheme;
import co.foldingmap.xml.XmlOutput;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Rectangle2D;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* VectorObject is the Parent Object for all of the different types
* of possible objects, MapPoint, Polygon, LineString, LinearRing, MultiGeometry
*
* @author Alec
*/
public abstract class VectorObject implements MapObject {
public static final byte CLAMP_TO_GROUND = 1;
public static final byte RELATIVE_TO_GROUND = 2;
public static final byte ABSOLUTE = 3;
//Listeners
protected ArrayList<ChangeObjectClassListener> classChangeListeners;
protected BasicStroke lineStyle;
protected boolean highlighted;
protected byte altitudeMode;
protected Coordinate selectedCoordinate;
protected HashMap<String, String> customDataFields;
protected Layer parentLayer;
protected LatLonAltBox boundingBox;
protected long reference, timestamp;
protected String objectDescription, objectName, objectClass;
protected CoordinateList<Coordinate> coordinates;
protected Visibility visibility;
//abstract methods
public abstract void drawOutline(Graphics2D g2, MapView mapView, boolean inMultiGeometry);
public abstract VectorObject fitToBoundry(LatLonAltBox boundry) throws ObjectNotWithinBoundsException;
public abstract Coordinate getCoordinateWithinRectangle(Rectangle2D range);
public abstract void toXML(XmlOutput kmlWriter);
/**
* Constructor to setup the default object settings, may or may not be
* called by children.
*
*/
public VectorObject() {
//Default Settings;
coordinates = new CoordinateList();
customDataFields = new HashMap<String, String>();
selectedCoordinate = Coordinate.UNKNOWN_COORDINATE;
reference = 0;
//initiate listener lists
classChangeListeners = new ArrayList<ChangeObjectClassListener>(3);
}
/**
* Adds a ChageObjectClassListener to this issue.
*
* @param listener
*/
public void addChangeObjectClassListener(ChangeObjectClassListener listener) {
classChangeListeners.add(listener);
}
/**
* Adds a custom data field to the object like City_population
*
* @param pvp The Property Value pair with the info.
*/
public void addCustomDataField(PropertyValuePair pvp) {
customDataFields.put(pvp.getProperty(), pvp.getValue());
}
/**
* Adds a custom data field to the object like City_population
*
* @param String The field name.
* @param String The field value.
*/
public void addCustomDataField(String field, String value) {
//remove old value
customDataFields.remove(field);
//insert new value
customDataFields.put(field, value);
}
/**
* Adds Custom Data Fields to the object.
*
* @param customDataFields An ArrayList of PropertyValuePairs to set as custom data fields.
*/
public void addCustomDataFields(ArrayList<PropertyValuePair> customDataFields) {
for (PropertyValuePair pvp: customDataFields)
this.customDataFields.put(pvp.getProperty(), pvp.getValue());
}
/**
* Adds a coordinate to the end of the coordinate list.
*
* @param c
*/
public void appendCoordinate(Coordinate c) {
coordinates.add(c);
generateBoundingBox();
}
/**
* Returns if this Vector Object is Equal to another object.
*
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
try {
if (o instanceof VectorObject) {
VectorObject vo = (VectorObject) o;
if (this.getClass() == vo.getClass()) {
return (this.hashCode() == vo.hashCode());
} else {
return false;
}
} else {
return false;
}
} catch (Exception e) {
Logger.log(Logger.ERR, "Error in VectorObject.equals(Object) - " + e);
return false;
}
}
/**
* Generates hash code for this object.
*
* @return
*/
@Override
public int hashCode() {
int hash = 7;
hash = 2 * hash + this.altitudeMode;
// hash = 89 * hash + (this.customDataFields != null ? this.customDataFields.hashCode() : 0);
hash = 4 * hash + (this.objectDescription != null ? this.objectDescription.hashCode() : 0);
hash = 6 * hash + (this.objectName != null ? this.objectName.hashCode() : 0);
hash = 8 * hash + (this.objectClass != null ? this.objectClass.hashCode() : 0);
hash = 10 * hash + (this.coordinates != null ? this.coordinates.hashCode() : 0);
return hash;
}
/**
* Creates a box that gives the bounds of the coordinates of this object.
*/
public void generateBoundingBox() {
float north = -90;
float south = 90;
float east = -180;
float west = 180;
float minAltitude = 0;
float maxAltitude = 0;
for (Coordinate c: coordinates) {
//East
if (c.getLongitude() > east)
east = (float) c.getLongitude();
//North
if (c.getLatitude() > north)
north = (float) c.getLatitude();
//south
if (c.getLatitude() < south)
south = (float) c.getLatitude();
//west
if (c.getLongitude() < west)
west = (float) c.getLongitude();
//Min Alt
if (c.getAltitude() < minAltitude)
minAltitude = c.getAltitude();
//Max Alt
if (c.getAltitude() > minAltitude)
minAltitude = c.getAltitude();
}
this.boundingBox = new LatLonAltBox(north, south, east, west, minAltitude, maxAltitude);
}
/**
* Returns an ArrayList of at the custom data associated with this object.
*
* @return
*/
public ArrayList<PropertyValuePair> getAllCustomData() {
ArrayList<PropertyValuePair> dataPairs;
Iterator it;
PropertyValuePair currentDataPair;
Set set;
dataPairs = new ArrayList<PropertyValuePair>();
set = customDataFields.entrySet();
it = set.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
currentDataPair = new PropertyValuePair((String) entry.getKey(), (String) entry.getValue());
dataPairs.add(currentDataPair);
}
return dataPairs;
}
/**
* Gets all the custom data field names associated with this object.
*
* @return Vector<String> A Vector containing all of the Custom Field Names.
*/
public ArrayList<String> getAllCustomDataFields() {
Set set = customDataFields.entrySet();
Iterator it = set.iterator();
ArrayList<String> fields = new ArrayList<String>();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
fields.add((String) entry.getKey());
}
return fields;
}
/**
* Returns the altitudeMode for this object.
* Possible values are CLAMP_TO_GROUND, RELATIVE_TO_GROUND, ABSOLUTE
*
* @return
*/
public byte getAltitudeMode() {
return altitudeMode;
}
/**
* Returns this objects bounding box.
*
* @return Bounding box as a LAtLonAltBox
*/
public LatLonAltBox getBoundingBox() {
if (this.boundingBox == null)
generateBoundingBox();
return this.boundingBox;
}
/**
* Returns the Coordinates with screen points within the given Rectangle2D.
*
* @param range
* @return
*/
public CoordinateList<Coordinate> getCoordinatesInRange(Rectangle2D range) {
CoordinateList<Coordinate> returnCoordates;
returnCoordates = new CoordinateList<Coordinate>();
for (Coordinate c: this.coordinates) {
if (range.contains(c.getCenterPoint()) ||
range.contains(c.getLeftPoint()) ||
range.contains(c.getRightPoint())) {
returnCoordates.add(c);
}
}
return returnCoordates;
}
/**
* Returns this VectorObject's CoordinateList
*
* @return
*/
@Override
public CoordinateList<Coordinate> getCoordinateList() {
return coordinates;
}
/**
* Returns this VectorObject's Coordinates as a String of the form:
* longitude,latitude,altitude longitude,latitude,altitude
*
* @return
*/
public String getCoordinateString() {
return coordinates.getCoordinateString();
}
/**
* Returns the value for a specified custom field name
*
* @param String The field name for the associated value.
* @return String The value for the passed in fieldName.
*/
public String getCustomDataFieldValue(String fieldName) {
String value;
if (fieldName.equalsIgnoreCase("Altitude")) {
value = Double.toString(coordinates.get(0).getAltitude());
} else {
value = customDataFields.get(fieldName);
}
return value;
}
/**
* Returns a HashMap of all the CustomeDataFields
*
* @return HashMap<String, String> The custom data fields.
*/
public HashMap<String, String> getCustomDataFields() {
return customDataFields;
}
/**
* Returns this VectorObject's description.
*
* @return
*/
@Override
public String getDescription() {
if (objectDescription != null) {
if (objectDescription.equalsIgnoreCase("null")) {
return "";
} else {
return objectDescription;
}
} else {
return "";
}
}
/**
* Returns the distance from the closest coordinate in the object to the
* one provided.
*
* @param c The Coordinate to compare.
* @return The distance from the closest Coordinate and the compare Coordinate.
*/
public double getDistanceFrom(Coordinate c) {
Coordinate closestCoordinate;
double distance;
distance = Double.MAX_VALUE;
try {
closestCoordinate = coordinates.getCoordinateClosestTo(c);
distance = CoordinateMath.getDistance(closestCoordinate, c);
} catch (Exception e) {
System.err.println("Error in MapObject.getDistanceFrom(Coordinate) - " + e);
}
return distance;
}
/**
* Returns all of this VectorObject's Coordinates as MapPoints.
* Used in Merging and un-merging operations.
*
* @return
*/
public VectorObjectList getMapPoints() {
VectorObjectList mapPoints;
Coordinate currentCoordinate;
MapPoint currentMapPoint;
mapPoints = new VectorObjectList();
for (int i = 0; i < coordinates.size(); i++) {
currentCoordinate = coordinates.get(i);
currentMapPoint = new MapPoint((objectName + " " + i), "(Unspecified Point)", "", currentCoordinate);
mapPoints.add(currentMapPoint);
}
return mapPoints;
}
/**
* Returns this Objects Class.
* Example: Highway, Cafe, Lake
*
* @return
*/
public String getObjectClass() {
return this.objectClass;
}
/**
* Returns this Objects Parent Layer
*
* @return
*/
@Override
public Layer getParentLayer() {
return parentLayer;
}
/**
* Returns the shape to be used for drawing component coordinates in
* Vector Objects.
*
* @param c The component Coordinate
* @param wrap The map wrap to be used, from MapView Class
* @return
*/
public static Shape getPointShape(Coordinate c, int wrap) {
Shape pointShape = null;
try {
if (wrap == MapView.WRAP_LEFT) {
pointShape = new Rectangle2D.Double((c.leftPoint.getX() - 1), (c.leftPoint.getY() - 1), 2, 2 );
} else if (wrap == MapView.WRAP_RIGHT) {
pointShape = new Rectangle2D.Double((c.rightPoint.getX() - 1), (c.rightPoint.getY() - 1), 2, 2 );
} else {
//default to center point
pointShape = new Rectangle2D.Double((c.centerPoint.getX() - 1), (c.centerPoint.getY() - 1), 2, 2 );
}
} catch (Exception e) {
System.err.println("Error in VectorObject.getPointShape(Coordiante, int) - " + e);
}
return pointShape;
}
/**
* Returns the object's description with any variables evaluated, this is used for showing
* popup content.
*
* @return
*/
public String getPopupDescription() {
//TODO: Add code to replace variables
String desc;
String replace = "\\" + "\"";
if (objectDescription != null) {
//Escape quotes and remove newlines and tabs
desc = objectDescription.replace("\"", replace);
desc = desc.replace("\n", "");
desc = desc.replace("\t", "");
} else {
desc = "";
}
return desc;
}
/**
* Returns the object reference for this object.
* @return
*/
public long getReference() {
return this.reference;
}
/**
* Returns this Objects Name.
*
* @return
*/
@Override
public String getName() {
if (objectName != null) {
return objectName;
} else {
return "";
}
}
/**
* Returns which coordinate, if any is selected within this map object.
* If no object is selected a null is returned.
*
* @return Coordinate The selected coordinate within this object.
*/
@Override
public Coordinate getSelectedCoordinate() {
return selectedCoordinate;
}
/**
* Returns this objects timestamp.
*
* @return The timestamp in yyyy-mm-ddThh:mm:ssZ format
*/
public String getTimestamp() {
SimpleDateFormat timestampDateFormat;
timestampDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ss");
timestampDateFormat.setTimeZone(new SimpleTimeZone(0, "Z"));
return timestampDateFormat.format(timestamp) + "Z";
}
/**
* Returns this Objects Visibility if it has one.
*
* @return
*/
public Visibility getVisibility() {
return visibility;
}
/**
* Tests to see if the given text has displayable text or just HTML
*
* @param text
* @return
*/
public static boolean hasDisplayableText(String text) {
boolean returnValue = true;
if (text.trim().equals("")) {
returnValue = false;
} else if (text.equalsIgnoreCase("<body></body>")) {
returnValue = false;
} else if (text.equalsIgnoreCase("<body><p style=\"margin-top: 0\"></p style=\"margin-top: 0\"></body>")) {
returnValue = false;
}
return returnValue;
}
/**
* Returns if this MapObjet is highlighted, i.e. Selected.
*
* @return
*/
@Override
public boolean isHighlighted() {
return this.highlighted;
}
/**
* Returns if this object should be visible on the map with the current MapView.
*
* @param mapView
* @return
*/
public boolean isVisible(MapView mapView) {
boolean visable = true;
if (this.visibility != null) {
visable = this.visibility.isVisible(mapView.getZoomLevel());
} else {
//check for style visability
ColorStyle cs = mapView.getMapTheme().getStyle(this.objectClass);
if (cs != null) {
Visibility v = cs.getVisibility();
if (v != null) {
return cs.getVisibility().isVisible(mapView.getZoomLevel());
} else {
return true;
}
} else {
return true;
}
}
return visable;
}
/**
* Adds a coordinate to the beginning of the coordinate list
*
* @param c
*/
public void prependCoordinate(Coordinate c) {
coordinates.add(0, c);
generateBoundingBox();
}
/**
* Removes a custom data field from this object.
*
* @param field
*/
public void removeCustomDataField(String field) {
this.customDataFields.remove(field);
}
/**
* Sets the altitudeMode for this Object.
*
* @param altitudeMode
*/
public void setAltitudeMode(byte altitudeMode) {
this.altitudeMode = altitudeMode;
}
/**
* Sets the class associated with this object
*/
public void setClass(String objectClass) {
String oldClass = this.objectClass;
this.objectClass = objectClass;
//Call Listeners, if there are any
if (classChangeListeners != null) {
for (ChangeObjectClassListener listener: classChangeListeners)
listener.objectClassChanged(this, oldClass, objectClass);
}
}
/**
* Sets this Objects CoordinateList. The Coordinate list is what keeps
* track of the objects Latitude, Longitude and Altitude Coordinates.
*
* @param newCoordinateList
*/
public void setCoordinateList(CoordinateList<Coordinate> newCoordinateList) {
this.coordinates = newCoordinateList;
generateBoundingBox();
}
/**
* Adds a custom data field to the object like City_population
*
* @param String The field name.
* @param String The field value.
*/
public void setCustomDataField(String field, String value) {
if (!field.equalsIgnoreCase("Name")) {
//remove old value
customDataFields.remove(field);
//insert new value
customDataFields.put(field, value);
} else {
this.setName(value);
}
}
/**
* Sets the Custom Data Field HashMap for this VectorObject.
*
* @param customDataFields
*/
public void setCustomDataFields(HashMap<String, String> customDataFields) {
this.customDataFields = customDataFields;
}
/**
* Sets this VectorObject's description
*
* @param description
*/
@Override
public void setDescription(String description) {
if (description.startsWith("<![CDATA[") ) {
//removes the cdata tag
this.objectDescription = description.substring(9, (description.length() - 3));
} else {
this.objectDescription = description;
}
}
/**
* Sets if this VectorObject is highlighted i.e. selected.
*
* @param h
*/
@Override
public void setHighlighted(boolean h) {
this.highlighted = h;
}
/**
* Sets this VectorObject's name.
*
* @param name
*/
@Override
public void setName(String name) {
this.objectName = name;
}
/**
* Sets this Object's Parent Layer. This is usually called when adding a
* VectorObject to a Layer Object.
*
* @param parentLayer
*/
@Override
public void setParentLayer(Layer parentLayer) {
this.parentLayer = parentLayer;
}
/**
* Sets the long value that can be used to reference an object.
*
* @param reference
*/
public void setReference(long reference) {
this.reference = reference;
}
/**
* Sets the selected coordinate within the VectorObject
*
*/
@Override
public void setSelectedCoordinate(Coordinate selectedCoordinate) {
if (coordinates.contains(selectedCoordinate)) {
this.selectedCoordinate = selectedCoordinate;
} else {
this.selectedCoordinate = coordinates.getCoordinateClosestTo(selectedCoordinate);
}
}
/**
* Sets the Timestamps for all of the coordinates in this object
*
* @param timestamps A string of space delimited time stamps in the form: yyyy-MM-dd'T'HH:mm:ss
*/
public void setTimestamps(String timestamps) {
StringTokenizer timestampTokenizer;
timestampTokenizer = new StringTokenizer(timestamps);
for (Coordinate currentCoordinate: coordinates) {
if (timestampTokenizer.hasMoreTokens()) {
currentCoordinate.setTimestamp(timestampTokenizer.nextToken());
} else {
//timestamp coordinate missmatch
System.err.println("Error in MapObject.setTimestamps(String) - Coordinate - Timestamp Missmatch");
}
}
}
/**
* Sets a Region for this VectorObject.
*
* @param region
*/
public void setVisibility(Visibility visibility) {
this.visibility = visibility;;
}
/**
* Un-selects the selected coordinate from this VectorObject
*/
@Override
public void unselectCoordinate() {
selectedCoordinate = null;
}
/**
* Updates the drawing of outlines, not used in all objects.
*
* @param theme
*/
public void updateOutlines(MapTheme theme) {
}
/**
* Writes all of the custom data fields as XML to the KMLWriter
*
* @param kmlWriter The writer to write xml to.
*/
public void writeCustomDataFieldsAsXML(XmlOutput xmlBuffer) {
Set set = customDataFields.entrySet();
Iterator it = set.iterator();
if (it.hasNext()) {
xmlBuffer.openTag("data");
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
xmlBuffer.writePairTag((String)entry.getKey(), (String) entry.getValue());
}
xmlBuffer.closeTag("data");
}
}
}