content: package aimax.osm.viewer;
  
  import aimax.osm.data.Position;
  import aimax.osm.data.WayNodeProvider;
  import aimax.osm.data.entities.MapEntity;
  import aimax.osm.data.entities.MapNode;
  import aimax.osm.data.entities.MapWay;
  import aimax.osm.data.entities.Track;
  
  import java.util.*;
  
  /**
   * Provides a rather general map entity renderer implementation. It assumes that
   * all entities provided to the renderer have an attached
   * {@link aimax.osm.viewer.DefaultEntityViewInfo} object. The visual appearance
   * of the drawn map strongly depends on those objects.
   * 
   * @author Ruediger Lunde
   */
  public class DefaultEntityRenderer extends AbstractEntityRenderer {
  
  	private Hashtable<Long, List<MapNode>> wayNodeHash;
  
  	/**
  	 * Default size used for fonts (in logical units).
  	 */
  	protected float defaultFontSize = 12f;
  
  	protected float scale;
  	protected float displayFactorSym;
  
  	protected List<MapWay> areaBuffer;
  	protected List<MapWay> wayBuffer;
  	protected List<MapEntity> nodeBuffer;
  	protected List<Track> trackBuffer;
  	protected List<NameInfo> nameInfoBuffer;
  	private List<MapNode> tmpNodeBuffer; // to improve thread-safety
  
  	/** Standard constructor. */
  	public DefaultEntityRenderer() {
  		wayNodeHash = new Hashtable<>();
  		setBackgroundColor(UColor.WHITE);
  		areaBuffer = new ArrayList<>();
  		wayBuffer = new ArrayList<>();
  		nodeBuffer = new ArrayList<>();
  		trackBuffer = new ArrayList<>();
  		nameInfoBuffer = new ArrayList<>();
  		tmpNodeBuffer = new ArrayList<>();
  	}
  
  	/** Clears all buffers and prepares rendering. */
  	@Override
  	public void initForRendering(UnifiedImageBuilder<?> imageBdr,
  			CoordTransformer transformer, WayNodeProvider wnProvider) {
  		super.initForRendering(imageBdr, transformer, wnProvider);
  		wayNodeHash.clear();
  
  		scale = transformer.computeScale();
  		displayFactorSym = displayFactor * transformer.getDotsPerUnit();
  		imageBdr.setFontSize(defaultFontSize * displayFactorSym);
  		areaBuffer.clear();
  		wayBuffer.clear();
  		nodeBuffer.clear();
  		trackBuffer.clear();
  		nameInfoBuffer.clear();
  		// count = 0;
  	}
  
  	protected List<MapNode> getWayNodes(MapWay way) {
  		List<MapNode> result = wayNodeHash.get(way.getId());
  		if (result == null) {
  			result = wnProvider.getWayNodes(way, scale);
  			wayNodeHash.put(way.getId(), result);
  		}
  		return result;
  	}
  
  	/**
  	 * Returns the map node which is the nearest with respect to the specified
  	 * view coordinates among the currently displayed nodes.
  	 */
  	public MapNode getNextNode(int x, int y) {
  		Position pos = new Position(transformer.lat(y), transformer.lon(x));
  		MapNode nextNode = null;
  		MapNode tmp;
  		for (int i = 0; i < 2; i++) {
  			List<MapWay> ways = (i == 0) ? areaBuffer : wayBuffer;
  			for (MapWay way : ways) {
  				tmp = pos.selectNearest(way.getNodes(), null);
  				if (nextNode == null
  						|| pos.getDistKM(tmp) < pos.getDistKM(nextNode)) {
  					nextNode = tmp;
  				}
  			}
  		}
  		for (MapEntity node : nodeBuffer) {
  			if (node instanceof MapNode) {
  				tmp = (MapNode) node;
  				if (tmp.getAttributeValue("marker") == null
  						&& (nextNode == null || pos.getDistKM(tmp) < pos.getDistKM(nextNode))) {
  					nextNode = tmp;
  				}
  			}
  		}
  		return nextNode;
  	}
  
  	/** Adds a way to a buffer for printing. */
  	@Override
  	public void visitMapWay(MapWay way) {
  		DefaultEntityViewInfo pInfo = (DefaultEntityViewInfo) way.getViewInfo();
  		List<MapNode> nodes = getWayNodes(way);
  		if (!nodes.isEmpty() && pInfo.wayColor != null) {
  			if (pInfo.wayFillColor != null
  					&& nodes.get(0) == nodes.get(nodes.size() - 1)
  					&& (way.isArea() || !pInfo.fillAreasOnly))
  				// alternative solution:
  				// && (way.isArea() ||
  				// (nodes.get(0) == nodes.get(nodes.size()-1)
  				// && !pInfo.fillAreasOnly)))
  				areaBuffer.add(way);
  			else
  				wayBuffer.add(way);
  		}
  		if (pInfo.isWayIcon && pInfo.icon != null)
  			nodeBuffer.add(way);
  	}
  
  	/** Adds the entity to a buffer for printing. */
  	@Override
  	public void visitMapNode(MapNode node) {
  		nodeBuffer.add(node);
  	}
  
  	/** Classifies the entity and possibly adds it to a buffer for printing. */
  	@Override
  	public void visitTrack(Track track) {
  		DefaultEntityViewInfo vInfo = (DefaultEntityViewInfo) track
  				.getViewInfo();
  		if (vInfo != null && scale >= vInfo.minVisibleScale * displayFactor) {
  			trackBuffer.add(track);
  		}
  	}
  
  	// int awnodes = 0;
  	/** Prints all buffered entities according to their rendering informations. */
  	public void printBufferedObjects() {
  		areaBuffer.sort(new MapAreaComparator());
  		Comparator<MapEntity> comp = new MapEntityComparator();
  		if (wayBuffer.size() < 10000)
  			wayBuffer.sort(comp);
  		if (nodeBuffer.size() < 10000)
  			nodeBuffer.sort(comp);
  		for (MapWay area : areaBuffer)
  			printWay(area, (DefaultEntityViewInfo) area.getViewInfo(), true);
  		for (MapWay way : wayBuffer)
  			printWay(way, (DefaultEntityViewInfo) way.getViewInfo(), false);
  		for (MapEntity node : nodeBuffer) {
  			MapNode n;
  			if (node instanceof MapWay) {
  				List<MapNode> wayNodes = getWayNodes((MapWay) node);
  				// needed to show icons for ways, whose abstraction is empty.
  				if (wayNodes.isEmpty())
  					wayNodes = ((MapWay) node).getNodes();
  				n = wayNodes.get(0);
  			} else
  				n = (MapNode) node;
  			printNode(n, (DefaultEntityViewInfo) node.getViewInfo());
  		}
  		for (Track track : trackBuffer)
  			printTrack(track);
  		// System.out.print("NamesOrg: " + nameInfoBuffer.size() + "\n");
  		Collections.sort(nameInfoBuffer);
  		// remove names whose positions are to close to each other
  		int charSize = (int) (defaultFontSize * displayFactorSym);
  		for (int i = 0; i < nameInfoBuffer.size(); ++i) {
  			NameInfo info = nameInfoBuffer.get(i);
  			for (int j = 0; j < i; ++j) {
  				NameInfo info1 = nameInfoBuffer.get(j);
  				int fac = (info.name.equals(info1.name)) ? 3 : 2;
  				if (Math.abs(info.y - info1.y) < charSize * fac) {
  					fac = (info.x < info1.x) ? info.name.length() : info1.name
  							.length();
  					if (Math.abs(info.x - info1.x) < charSize * fac) {
  						nameInfoBuffer.remove(i);
  						--i;
  						j = i;
  					}
  				}
  			}
  		}
  		for (NameInfo textInfo : nameInfoBuffer) {
  			imageBdr.setColor(textInfo.color);
  			imageBdr.drawString(textInfo.name, textInfo.x, textInfo.y);
  		}
  		// System.out.print("Areas: " + areaBuffer.size() + "  ");
  		// System.out.print("Ways: " + wayBuffer.size() + "  ");
  		// System.out.print("Nodes: " + nodeBuffer.size() + "  ");
  		// System.out.print("Names: " + nameInfoBuffer.size() + "\n");
  	}
  
  	/** Prints a way entity. */
  	protected void printWay(MapWay way, DefaultEntityViewInfo pInfo,
  			boolean asArea) {
  		List<MapNode> nodes = getWayNodes(way);
  		if (nodes != null) {
  			// awnodes+=nodes.size();
  			boolean asOneway = false;
  			NameInfo textInfo = null;
  			if (scale >= pInfo.minNameScale * displayFactor) {
  				asOneway = way.isOneway();
  				if (way.getName() != null && pInfo.nameColor != null) {
  					textInfo = new NameInfo(way.getName(), pInfo.nameColor,
  							pInfo.printOrder);
  				}
  			}
  			printLine(imageBdr, nodes, pInfo, asArea, asOneway, textInfo);
  		}
  	}
  
  	/** Prints a node entity. */
  	protected void printNode(MapNode node, DefaultEntityViewInfo pInfo) {
  		int x = transformer.x(node.getLon());
  		int y = transformer.y(node.getLat());
  		int width = 0;
  
  		if (pInfo.icon != null) {
  			width = Math.round(pInfo.icon.size * displayFactorSym);
  			pInfo.icon.draw(imageBdr, x, y, displayFactorSym);
  		}
  
  		if (scale >= pInfo.minNameScale * displayFactor) {
  			String name = node.getName();
  			if (name != null && pInfo.nameColor != null) {
  				NameInfo info = new NameInfo(name, pInfo.nameColor,
  						pInfo.printOrder);
  				info.x = x + width;
  				info.y = y + width / 4;
  				nameInfoBuffer.add(info);
  			}
  		}
  	}
  
  	/** Prints a track entity. */
  	protected void printTrack(Track track) {
  		DefaultEntityViewInfo vInfo = (DefaultEntityViewInfo) track
  				.getViewInfo();
  		tmpNodeBuffer.clear();
  		tmpNodeBuffer.addAll(track.getNodes());
  		if (!tmpNodeBuffer.isEmpty()) {
  			printLine(imageBdr, tmpNodeBuffer, vInfo, false, false, null);
  			printPoint(imageBdr, tmpNodeBuffer.get(tmpNodeBuffer.size() - 1),
  					vInfo, null);
  		}
  	}
  
  	/** Prints a line or fills an area. */
  	protected void printLine(UnifiedImageBuilder<?> imageBdr, List<MapNode> nodes,
  			DefaultEntityViewInfo pInfo, boolean asArea, boolean asOneway,
  			NameInfo textInfo) {
  		// count++;
  		int[] xPoints = new int[nodes.size()];
  		int[] yPoints = new int[nodes.size()];
  
  		int viewWidth = !asArea ? imageBdr.getWidth() : -1;
  		int viewHeight = !asArea ? imageBdr.getHeight() : -1;
  		boolean visible = getViewCoords(nodes, viewWidth, viewHeight, xPoints,
  				yPoints);
  
  		if (visible) {
  			boolean filled = false;
  			if (asArea) {
  				imageBdr.setColor(pInfo.wayFillColor != null ? pInfo.wayFillColor
  						: pInfo.wayColor);
  				imageBdr.setLineStyle(false, displayFactor);
  				imageBdr.setAreaFilled(true);
  				imageBdr.drawPolygon(xPoints, yPoints, nodes.size());
  				filled = true;
  			}
  			if (!filled || pInfo.wayFillColor != null
  					&& !pInfo.wayFillColor.equals(pInfo.wayColor)) {
  				imageBdr.setColor(pInfo.wayColor);
  				imageBdr.setLineStyle(pInfo.wayDashed, pInfo.wayWidth
  						* displayFactorSym);
  				imageBdr.setAreaFilled(false);
  				imageBdr.drawPolyline(xPoints, yPoints, nodes.size());
  			}
  			if (asOneway) {
  				float x = xPoints[xPoints.length - 1];
  				float y = yPoints[yPoints.length - 1];
  				double angle = Math.atan2(x - xPoints[xPoints.length - 2],
  						-(y - yPoints[yPoints.length - 2]));
  				printOnewayArrow(x, y, angle);
  			}
  			if (textInfo != null) {
  				setWayNamePosition(textInfo, xPoints, yPoints, filled);
  				nameInfoBuffer.add(textInfo);
  			}
  			if (debugMode && scale >= 2 * pInfo.minNameScale * displayFactor) {
  				int i = 0;
  				for (MapNode node : nodes) {
  					textInfo = new NameInfo(Long.toString(node.getId()),
  							pInfo.nameColor, pInfo.printOrder);
  					textInfo.x = xPoints[i];
  					textInfo.y = yPoints[i];
  					nameInfoBuffer.add(textInfo);
  					++i;
  				}
  			}
  		}
  	}
  
  	/**
  	 * Computes the view coordinates for a list of way nodes and checks
  	 * visibility with respect to a clipping rectangle. The check improves the
  	 * viewing performance in large scales in which long invisible ways (e.g.
  	 * coast lines) often pass the bounding box test.
  	 * 
  	 * @param nodes
  	 *            List of way nodes.
  	 * @param viewWidth
  	 *            Used for clipping if > 0.
  	 * @param viewHeight
  	 *            Used for clipping if > 0.
  	 * @param xView
  	 *            Array of coordinates for the result.
  	 * @param yView
  	 *            Array of coordinates for the result.
  	 * @return true if at least a part of the line is visible.
  	 */
  	protected boolean getViewCoords(List<MapNode> nodes, int viewWidth,
  			int viewHeight, int[] xView, int[] yView) {
  		boolean visible = (viewWidth <= 0 || viewHeight <= 0);
  		int xv;
  		int yv;
  		int xClipPos;
  		int yClipPos;
  		int xClipPosLast = 0;
  		int yClipPosLast = 0;
  		int i = 0;
  		for (MapNode node : nodes) {
  			xv = transformer.x(node.getLon());
  			yv = transformer.y(node.getLat());
  			// bounding box test not sufficient for large scales...
  			xView[i] = xv;
  			yView[i] = yv;
  			if (!visible) {
  				xClipPos = 0;
  				if (xv < 0)
  					xClipPos = 1;
  				else if (xv > viewWidth)
  					xClipPos = 2;
  				yClipPos = 0;
  				if (yv < 0)
  					yClipPos = 1;
  				else if (yv > viewHeight)
  					yClipPos = 2;
  				visible = (xClipPos == 0 || xClipPos != xClipPosLast && i > 0)
  						&& (yClipPos == 0 || yClipPos != yClipPosLast && i > 0);
  				xClipPosLast = xClipPos;
  				yClipPosLast = yClipPos;
  			}
  			++i;
  		}
  		return visible;
  	}
  
  	/**
  	 * Marks the end of a one-way street with an arrow. The angle specifies the
  	 * direction, zero means north.
  	 */
  	protected void printOnewayArrow(float x, float y, double angle) {
  		imageBdr.setColor(UColor.GRAY);
  		imageBdr.setLineStyle(false, displayFactorSym);
  		imageBdr.setAreaFilled(false);
  		drawArrowLine(x, y, displayFactorSym * 10f, angle);
  		drawArrowLine(x, y, displayFactorSym * 7f, angle - Math.PI/4);
  		drawArrowLine(x, y, displayFactorSym * 7f, angle + Math.PI/4);
  	}
  
  	private void drawArrowLine(float x, float y, float length, double angle) {
  		imageBdr.drawLine(Math.round(x), Math.round(y),
  				(int) Math.round(-length * Math.sin(angle) + x),
  				(int) Math.round(length * Math.cos(angle) + y));
  	}
  
  	/** Finds a good place for printing the name of a way entity. */
  	protected void setWayNamePosition(NameInfo info, int[] xPoints,
  			int[] yPoints, boolean area) {
  		int x = 0;
  		int y = 0;
  		int max = debugMode ? 1 : (area ? xPoints.length : 2);
  		for (int i = 0; i < max; i++) {
  			x += xPoints[i];
  			y += yPoints[i];
  		}
  		info.x = x / max;
  		info.y = y / max;
  		if (debugMode)
  			info.y += defaultFontSize * displayFactorSym;
  	}
  
  	/** Prints a point of interest. */
  	protected void printPoint(UnifiedImageBuilder<?> imageBdr, MapNode node,
  			DefaultEntityViewInfo pInfo, UColor nameColor) {
  		int x = transformer.x(node.getLon());
  		int y = transformer.y(node.getLat());
  		int width = 0;
  
  		if (pInfo.icon != null) {
  			width = Math.round(pInfo.icon.size * displayFactorSym);
  			pInfo.icon.draw(imageBdr, x, y, displayFactor);
  		}
  
  		if (nameColor != null) {
  			String name = (debugMode) ? "P" + node.getId() : node.getName();
  			if (name != null) {
  				NameInfo info = new NameInfo(name, nameColor, pInfo.printOrder);
  				info.x = x + width;
  				info.y = y + width / 4;
  				nameInfoBuffer.add(info);
  			}
  		}
  	}
  
  	// ////////////////////////////////////////////////////////////////////
  	// some inner classes...
  
  	/**
  	 * Stores color and position information for a name to be printed out.
  	 */
  	protected static class NameInfo implements Comparable<NameInfo> {
  		public String name;
  		public UColor color;
  		public int x;
  		public int y;
  		/** Print order value of the corresponding entity. */
  		public int printOrder;
  
  		protected NameInfo(String name, UColor color, int printOrder) {
  			this.name = name;
  			this.color = color;
  			this.printOrder = printOrder;
  		}
  
  		@Override
  		public int compareTo(NameInfo arg0) {
  			return Integer.compare(printOrder, arg0.printOrder);
  		}
  	}
  
  	/** Compares entity print informations with respect to print order. */
  	protected static class MapEntityComparator implements Comparator<MapEntity> {
  		@Override
  		public int compare(MapEntity arg0, MapEntity arg1) {
  			DefaultEntityViewInfo info0 = (DefaultEntityViewInfo) arg0
  					.getViewInfo();
  			DefaultEntityViewInfo info1 = (DefaultEntityViewInfo) arg1
  					.getViewInfo();
  			return Integer.compare(info1.printOrder, info0.printOrder);
  		}
  	}
  
  	/** Compares ways with respect to the size of their bounding box. */
  	protected static class MapAreaComparator implements Comparator<MapWay> {
  		@Override
  		public int compare(MapWay arg0, MapWay arg1) {
  			return -Float.compare(arg0.getBoundingBoxSize(),
  					arg1.getBoundingBoxSize());
  		}
  	}
  }
  