package aimax.osm.data;

import java.util.Collection;
import java.util.List;

import aimax.osm.data.entities.MapEntity;
import aimax.osm.data.entities.MapNode;
import aimax.osm.data.entities.MapWay;
import aimax.osm.data.entities.WayRef;

/**
 * Maintains a latitude longitude pair and provides some useful methods for
 * distance calculations.
 * 
 * @author Ruediger Lunde
 */
public class Position {

	/** Earth's mean radius in km. */
	public static double EARTH_RADIUS = 6371.0;
	protected float lat;
	protected float lon;

	public Position(float lat, float lon) {
		this.lat = lat;
		this.lon = lon;
	}

	public Position(MapNode node) {
		lat = node.getLat();
		lon = node.getLon();
	}

	public float getLat() {
		return lat;
	}

	public float getLon() {
		return lon;
	}

	/**
	 * Computes the distance in kilometer from this position to a specified map
	 * node.
	 */
	public double getDistKM(MapEntity entity) {
		if (entity instanceof MapNode) {
			return getDistKM(lat, lon, ((MapNode) entity).getLat(),
					((MapNode) entity).getLon());
		} else if (entity instanceof MapWay) {
			MapWay way = (MapWay) entity;
			BoundingBox bb = way.computeBoundingBox();
			float bbLat = (Math.abs(lat - bb.getLatMin()) < Math.abs(lat
					- bb.getLatMax())) ? bb.getLatMin() : bb.getLatMax();
			float bbLon = (Math.abs(lon - bb.getLonMin()) < Math.abs(lon
					- bb.getLonMax())) ? bb.getLonMin() : bb.getLonMax();
			return getDistKM(lat, lon, bbLat, bbLon);
		}
		return Double.NaN;
	}

	public boolean insertInAscendingDistanceOrder(List<MapEntity> nodes,
			MapNode node) {
		int pos = getInsertPosition(nodes, node);
		if (pos != -1)
			nodes.add(pos, node);
		return pos != -1;
	}
	
	public boolean insertInAscendingDistanceOrder(List<MapEntity> ways,
			MapWay way) {
		int pos = getInsertPosition(ways, way);
		if (pos != -1)
			ways.add(pos, way);
		return pos != -1;
	}
	
	private int getInsertPosition(List<?> entities, MapEntity entity) {
		int pos1 = 0;
		int pos2 = entities.size();
		double newDistance = getDistKM(entity);
		while (pos1 < pos2) {
			int pos3 = (pos1 + pos2) / 2;
			double dist3 = getDistKM((MapEntity) entities.get(pos3));
			if (newDistance < dist3)
				pos2 = pos3;
			else if (newDistance > dist3)
				pos1 = pos3 + 1;
			else
				pos1 = pos2 = pos3;
		}
		if (pos1 < entities.size()) {
			for (int i = pos1; i >= 0
					&& getDistKM((MapEntity) entities.get(i)) == newDistance; i--)
				if (entities.get(i) == entity)
					return -1;
			for (int i = pos1 + 1; i < entities.size()
					&& getDistKM((MapEntity) entities.get(i)) == newDistance; i++)
				if (entities.get(i) == entity)
					return -1;
		}
		return pos1;
	}

	/**
	 * Returns the node from <code>nodes</code> which is nearest to this
	 * position. If a filter is given, only those nodes are inspected, which are
	 * part of a way accepted by the filter.
	 * 
	 * @param nodes
	 * @param filter
	 *            possibly null
	 * @return A node or null
	 */
	public MapNode selectNearest(Collection<MapNode> nodes, MapWayFilter filter) {
		MapNode result = null;
		double dist = Double.MAX_VALUE;
		double newDist;
		for (MapNode node : nodes) {
			newDist = getDistKM(node);
			boolean found = (newDist < dist);
			if (found && filter != null) {
				found = false;
				for (WayRef ref : node.getWayRefs()) {
					if (filter.isAccepted(ref.getWay()))
						found = true;
				}
			}
			if (found) {
				result = node;
				dist = newDist;
			}
		}
		return result;
	}

	/**
	 * Computes a simple approximation of the compass course from this position
	 * to the specified node.
	 * 
	 * @param node
	 * @return Number between 1 and 360
	 */
	public int getCourseTo(MapNode node) {
		double lonCorr = Math.cos(Math.PI / 360.0 * (lat + node.getLat()));
		double latDist = node.getLat() - lat;
		double lonDist = lonCorr * (node.getLon() - lon);
		int course = (int) (180.0 / Math.PI * Math.atan2(lonDist, latDist));
		if (course <= 0)
			course += 360;
		return course;
	}

	/** Computes the total length of a track in kilometers. */
	public static double getTrackLengthKM(List<MapNode> nodes) {
		double result = 0.0;
		for (int i = 1; i < nodes.size(); i++) {
			MapNode n1 = nodes.get(i - 1);
			MapNode n2 = nodes.get(i);
			result += getDistKM(n1.getLat(), n1.getLon(), n2.getLat(), n2
					.getLon());
		}
		return result;
	}

	/**
	 * Computes the distance between two positions on the earth surface using
	 * the haversine formula.
	 */
	public static double getDistKM(float lat1, float lon1, float lat2,
			float lon2) {
		double dLat = Math.toRadians(lat2 - lat1);
		double dLon = Math.toRadians(lon2 - lon1);
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
				+ Math.cos(Math.toRadians(lat1))
				* Math.cos(Math.toRadians(lat2)) * Math.sin(dLon / 2)
				* Math.sin(dLon / 2);
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
		return EARTH_RADIUS * c;
	}
}
