From 9109da97840405afd7479c010f8b5ba9439e1a41 Mon Sep 17 00:00:00 2001
From: Hannes Janetzek <hannes.janetzek@gmail.com>
Date: Tue, 21 Jan 2014 18:19:13 +0100
Subject: [PATCH] add vtm-jeo

---
 vtm-jeo/src/org/oscim/layers/JeoMapLayer.java |  80 ++++
 .../src/org/oscim/layers/JeoMapLoader.java    | 346 ++++++++++++++++++
 vtm-jeo/src/org/oscim/layers/JeoTestData.java | 128 +++++++
 .../org/oscim/theme/carto/MatcherFeature.java |  74 ++++
 .../org/oscim/theme/carto/RenderTheme.java    | 266 ++++++++++++++
 .../src/org/oscim/theme/carto/RuleDebug.java  |  74 ++++
 6 files changed, 968 insertions(+)
 create mode 100644 vtm-jeo/src/org/oscim/layers/JeoMapLayer.java
 create mode 100644 vtm-jeo/src/org/oscim/layers/JeoMapLoader.java
 create mode 100644 vtm-jeo/src/org/oscim/layers/JeoTestData.java
 create mode 100644 vtm-jeo/src/org/oscim/theme/carto/MatcherFeature.java
 create mode 100644 vtm-jeo/src/org/oscim/theme/carto/RenderTheme.java
 create mode 100644 vtm-jeo/src/org/oscim/theme/carto/RuleDebug.java

diff --git a/vtm-jeo/src/org/oscim/layers/JeoMapLayer.java b/vtm-jeo/src/org/oscim/layers/JeoMapLayer.java
new file mode 100644
index 00000000..083a11f0
--- /dev/null
+++ b/vtm-jeo/src/org/oscim/layers/JeoMapLayer.java
@@ -0,0 +1,80 @@
+package org.oscim.layers;
+
+import org.jeo.data.Dataset;
+import org.jeo.map.Style;
+import org.oscim.core.MapPosition;
+import org.oscim.layers.JeoMapLoader.Task;
+import org.oscim.map.Map;
+import org.oscim.map.Map.UpdateListener;
+import org.oscim.renderer.ElementRenderer;
+import org.oscim.renderer.MapRenderer.Matrices;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JeoMapLayer extends Layer implements UpdateListener {
+
+	public static final Logger log = LoggerFactory.getLogger(JeoMapLayer.class);
+
+	final org.jeo.map.View view;
+	private final org.jeo.map.Map mJeoMap;
+
+	private final JeoMapLoader mWorker;
+
+	public JeoMapLayer(Map map, Dataset data, Style style) {
+		super(map);
+
+		mJeoMap = org.jeo.map.Map.build().layer(data).style(style).map();
+		view = mJeoMap.getView();
+
+		mRenderer = new ElementRenderer() {
+			@Override
+			protected synchronized void update(MapPosition position, boolean changed,
+			        Matrices matrices) {
+
+				if (mNewLayers != null) {
+					mMapPosition.copy(mNewLayers);
+
+					this.layers.clear();
+					this.layers.baseLayers = mNewLayers.layers;
+					mNewLayers = null;
+
+					compile();
+					log.debug("is ready " + isReady() + " " + layers.getSize());
+				}
+			}
+		};
+
+		mWorker = new JeoMapLoader(this);
+		mWorker.start();
+	}
+
+	@Override
+	public void onDetach() {
+		super.onDetach();
+
+		mWorker.awaitPausing();
+		try {
+			mWorker.join();
+		} catch (Exception e) {
+			log.error(e.toString());
+		}
+	}
+
+	@Override
+	public void onMapUpdate(MapPosition pos, boolean changed, boolean clear) {
+		if (changed) {
+			log.debug("go");
+			mWorker.go();
+		}
+	}
+
+	Task mNewLayers;
+
+	void setLayers(Task newLayers) {
+		synchronized (mRenderer) {
+			mNewLayers = newLayers;
+		}
+		mMap.render();
+	}
+
+}
diff --git a/vtm-jeo/src/org/oscim/layers/JeoMapLoader.java b/vtm-jeo/src/org/oscim/layers/JeoMapLoader.java
new file mode 100644
index 00000000..ad7da174
--- /dev/null
+++ b/vtm-jeo/src/org/oscim/layers/JeoMapLoader.java
@@ -0,0 +1,346 @@
+package org.oscim.layers;
+
+// FIXME
+// Apache License 2.0
+
+import java.io.IOException;
+
+import org.jeo.data.Dataset;
+import org.jeo.data.Query;
+import org.jeo.data.VectorDataset;
+import org.jeo.feature.Feature;
+import org.jeo.geom.CoordinatePath;
+import org.jeo.geom.Envelopes;
+import org.jeo.geom.Geom;
+import org.jeo.map.CartoCSS;
+import org.jeo.map.Map;
+import org.jeo.map.RGB;
+import org.jeo.map.Rule;
+import org.jeo.map.RuleList;
+import org.jeo.map.View;
+import org.oscim.core.BoundingBox;
+import org.oscim.core.GeometryBuffer;
+import org.oscim.core.MapPosition;
+import org.oscim.core.MercatorProjection;
+import org.oscim.core.Tile;
+import org.oscim.renderer.elements.ElementLayers;
+import org.oscim.renderer.elements.LineLayer;
+import org.oscim.renderer.elements.MeshLayer;
+import org.oscim.renderer.elements.RenderElement;
+import org.oscim.theme.renderinstruction.Line;
+import org.oscim.utils.PausableThread;
+import org.oscim.utils.TileClipper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.vividsolutions.jts.geom.Coordinate;
+import com.vividsolutions.jts.geom.Envelope;
+import com.vividsolutions.jts.geom.Geometry;
+
+/**
+ * Does the work of actually rendering the map, outside of the ui thread.
+ * 
+ * @author Justin Deoliveira, OpenGeo
+ * @author Hannes Janetzek, OpenScienceMap
+ */
+
+public class JeoMapLoader extends PausableThread {
+
+	static final Logger log = LoggerFactory.getLogger(JeoMapLoader.class);
+
+	private final JeoMapLayer mMapLayer;
+
+	public JeoMapLoader(JeoMapLayer mapLayer) {
+		mMapLayer = mapLayer;
+	}
+
+	private ElementLayers layers;
+	private final GeometryBuffer mGeom = new GeometryBuffer(128, 4);
+
+	private Task mCurrentTask;
+
+	private double mMinX;
+	private double mMinY;
+
+	@Override
+	protected void doWork() throws InterruptedException {
+		log.debug("start");
+		mWork = false;
+		Envelope env = new Envelope();
+		BoundingBox bbox = mMapLayer.mMap.getViewport().getViewBox();
+
+		env.init(bbox.getMinLongitude(), bbox.getMaxLongitude(),
+		         bbox.getMinLatitude(), bbox.getMaxLatitude());
+		int w = mMapLayer.mMap.getWidth();
+		int h = mMapLayer.mMap.getHeight();
+		mMapLayer.view.setWidth(w);
+		mMapLayer.view.setHeight(h);
+
+		mClipper.setRect(-w, -h, w, h);
+
+		mMapLayer.view.zoomto(env);
+
+		Task task = new Task();
+		task.view = mMapLayer.view.clone();
+
+		mMapLayer.mMap.getMapPosition(task);
+
+		mCurrentTask = task;
+		layers = new ElementLayers();
+
+		Envelope b = task.view.getBounds();
+
+		// reduce lines points min distance
+		mMinX = ((b.getMaxX() - b.getMinX()) / task.view.getWidth()) * 2;
+		mMinY = ((b.getMaxY() - b.getMinY()) / task.view.getHeight()) * 2;
+
+		Map map = mMapLayer.view.getMap();
+
+		for (org.jeo.map.Layer l : map.getLayers()) {
+
+			if (!l.isVisible())
+				continue;
+
+			Dataset data = l.getData();
+
+			RuleList rules =
+			        map.getStyle().getRules().selectById(l.getName(), true).flatten();
+
+			log.debug("data {}", data);
+
+			if (data instanceof VectorDataset) {
+				for (RuleList ruleList : rules.zgroup()) {
+					render(task.view, (VectorDataset) data, ruleList);
+				}
+			}
+		}
+
+		if (layers.baseLayers != null) {
+			mCurrentTask.layers = layers.baseLayers;
+
+			//layers.baseLayers = null;
+			//layers.clear();
+
+			mMapLayer.setLayers(mCurrentTask);
+		}
+		layers = null;
+		mCurrentTask = null;
+
+	}
+
+	void render(View view, VectorDataset data, RuleList rules) {
+
+		try {
+			Query q = new Query().bounds(view.getBounds());
+			log.debug("query {}", q);
+
+			// reproject
+			// if (data.getCRS() != null) {
+			//	if (!Proj.equal(view.getCRS(), data.getCRS())) {
+			//		q.reproject(view.getCRS());
+			//	}
+			//}
+			//else {
+			//	log.debug("Layer " + data.getName()
+			//		+ " specifies no projection, assuming map projection");
+			//}
+
+			for (Feature f : data.cursor(q)) {
+
+				RuleList rs = rules.match(f);
+				if (rs.isEmpty()) {
+					continue;
+				}
+
+				Rule r = rules.match(f).collapse();
+				if (r == null)
+					continue;
+
+				draw(view, f, r);
+			}
+		} catch (IOException e) {
+			log.error("Error querying layer " + data.getName() + e);
+		}
+	}
+
+	Geometry clipGeometry(View view, Geometry g) {
+		// TODO: doing a full intersection is sub-optimal,
+		// look at a more efficient clipping
+		// algorithm, like cohen-sutherland
+		return g.intersection(Envelopes.toPolygon(view.getBounds()));
+	}
+
+	void draw(View view, Feature f, Rule rule) {
+		Geometry g = f.geometry();
+		if (g == null) {
+			return;
+		}
+
+		//		g = clipGeometry(view, g);
+		//		if (g.isEmpty()) {
+		//			return;
+		//		}
+
+		switch (Geom.Type.from(g)) {
+			case POINT:
+			case MULTIPOINT:
+				//log.debug("draw point");
+				//drawPoint(f, rule);
+				return;
+			case LINESTRING:
+			case MULTILINESTRING:
+				//log.debug("draw line");
+				drawLine(f, rule, g);
+				return;
+			case POLYGON:
+				//Polygon p = (Polygon) g;
+				//p.reverse();
+				//log.debug("draw polygon");
+				drawPolygon(f, rule, g);
+				return;
+
+			case MULTIPOLYGON:
+				//log.debug("draw polygon");
+				for (int i = 0, n = g.getNumGeometries(); i < n; i++)
+					drawPolygon(f, rule, g.getGeometryN(i));
+				return;
+			default:
+				throw new UnsupportedOperationException();
+		}
+	}
+
+	private void drawLine(Feature f, Rule rule, Geometry g) {
+
+		LineLayer ll = layers.getLineLayer(0);
+
+		if (ll.line == null) {
+			RGB color = rule.color(f, CartoCSS.LINE_COLOR, RGB.black);
+			float width = rule.number(f, CartoCSS.LINE_WIDTH, 1.2f);
+			ll.line = new Line(0, color(color), width);
+			ll.width = width;
+		}
+
+		mGeom.clear();
+		mGeom.startLine();
+
+		CoordinatePath p = CoordinatePath.create(g);
+		path(mGeom, p);
+
+		//log.debug( ll.width + " add line " + mGeom.pointPos + " " + Arrays.toString(mGeom.points));
+
+		ll.addLine(mGeom);
+	}
+
+	TileClipper mClipper = new TileClipper(0, 0, 0, 0);
+
+	private void drawPolygon(Feature f, Rule rule, Geometry g) {
+
+		LineLayer ll = layers.getLineLayer(3);
+
+		if (ll.line == null) {
+			RGB color = rule.color(f, CartoCSS.POLYGON_FILL, RGB.red);
+			float width = rule.number(f, CartoCSS.LINE_WIDTH, 1.2f);
+			ll.line = new Line(2, color(color), width);
+			ll.width = width;
+		}
+
+		//PolygonLayer pl = layers.getPolygonLayer(1);
+		//
+		//if (pl.area == null) {
+		//	RGB color = rule.color(f, CartoCSS.POLYGON_FILL, RGB.red);
+		//	pl.area = new Area(1, color(color));
+		//}
+
+		MeshLayer mesh = layers.getMeshLayer(2);
+
+		mGeom.clear();
+		mGeom.startPolygon();
+		//mGeom.startLine();
+
+		CoordinatePath p = CoordinatePath.create(g).generalize(mMinX, mMinY);
+		if (path(mGeom, p) < 3)
+			return;
+
+		if (!mClipper.clip(mGeom))
+			return;
+
+		//log.debug(ll.width + " add poly " + mGeom.pointPos + " " + Arrays.toString(mGeom.points));
+		mesh.addMesh(mGeom);
+
+		ll.addLine(mGeom);
+		//pl.addPolygon(mGeom.points, mGeom.index);
+	}
+
+	public static int color(RGB rgb) {
+		return rgb.getAlpha() << 24
+		        | rgb.getRed() << 16
+		        | rgb.getGreen() << 8
+		        | rgb.getBlue();
+	}
+
+	private int path(GeometryBuffer g, CoordinatePath path) {
+
+		MapPosition pos = mCurrentTask;
+		double scale = pos.scale * Tile.SIZE;
+		int cnt = 0;
+		O: while (path.hasNext()) {
+			Coordinate c = path.next();
+			float x = (float) ((MercatorProjection.longitudeToX(c.x) - pos.x) * scale);
+			float y = (float) ((MercatorProjection.latitudeToY(c.y) - pos.y) * scale);
+
+			switch (path.getStep()) {
+				case MOVE_TO:
+					if (g.isPoly())
+						g.startPolygon();
+					else if (g.isLine())
+						g.startLine();
+
+					cnt++;
+					g.addPoint(x, y);
+					break;
+
+				case LINE_TO:
+					cnt++;
+					g.addPoint(x, y);
+					break;
+
+				case CLOSE:
+					//g.addPoint(x, y);
+
+					//if (g.type == GeometryType.POLY)
+					break;
+				case STOP:
+					break O;
+			}
+		}
+		return cnt;
+	}
+
+	@Override
+	protected String getThreadName() {
+		return "JeoMapLayer";
+	}
+
+	@Override
+	protected boolean hasWork() {
+		return mWork;
+	}
+
+	boolean mWork;
+
+	public void go() {
+		if (hasWork())
+			return;
+
+		mWork = true;
+
+		synchronized (this) {
+			notifyAll();
+		}
+	}
+
+	static class Task extends MapPosition {
+		View view;
+		RenderElement layers;
+	}
+}
diff --git a/vtm-jeo/src/org/oscim/layers/JeoTestData.java b/vtm-jeo/src/org/oscim/layers/JeoTestData.java
new file mode 100644
index 00000000..d2be9c73
--- /dev/null
+++ b/vtm-jeo/src/org/oscim/layers/JeoTestData.java
@@ -0,0 +1,128 @@
+package org.oscim.layers;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.jeo.carto.Carto;
+import org.jeo.data.Dataset;
+import org.jeo.data.Query;
+import org.jeo.data.mem.MemVector;
+import org.jeo.data.mem.MemWorkspace;
+import org.jeo.feature.Feature;
+import org.jeo.feature.Features;
+import org.jeo.feature.Schema;
+import org.jeo.feature.SchemaBuilder;
+import org.jeo.geojson.GeoJSONDataset;
+import org.jeo.geom.GeomBuilder;
+import org.jeo.map.Style;
+
+import com.vividsolutions.jts.geom.Geometry;
+
+public class JeoTestData {
+
+	public static Style getStyle() {
+		Style style = null;
+
+		try {
+			style = Carto.parse("" +
+			        "#things {" +
+			        "  line-color: #c80;" +
+			        "  polygon-fill: #00a;" +
+			        "}" +
+			        "#states {" +
+			        "  polygon-fill: #0dc;" +
+			        "}"
+			    );
+
+			return style;
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+	public static Dataset getJsonData(String file, boolean memory) {
+		GeoJSONDataset data = null;
+
+		try {
+			data = new GeoJSONDataset(new File(file));
+		} catch (UnsupportedOperationException e) {
+			e.printStackTrace();
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+
+		if (memory) {
+			MemWorkspace mem = new MemWorkspace();
+
+			//mem.put("layer", data);
+			try {
+
+				Schema s = data.schema();
+				Query q = new Query();
+
+				MemVector memData = mem.create(s);
+
+				for (Feature f : data.cursor(q)) {
+					memData.add(f);
+				}
+
+				//return mem.get("layer");
+				return memData;
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+		return data;
+	}
+
+	public static Dataset getMemWorkspace(String layer) {
+		GeomBuilder gb = new GeomBuilder(4326);
+
+		MemWorkspace mem = new MemWorkspace();
+		Schema schema = new SchemaBuilder(layer)
+		    .field("geometry", Geometry.class)
+		    .field("id", Integer.class)
+		    .field("name", String.class)
+		    .field("cost", Double.class).schema();
+
+		MemVector data;
+		try {
+			data = mem.create(schema);
+		} catch (UnsupportedOperationException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+			return null;
+		} catch (IOException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+			return null;
+		}
+
+		Geometry g = gb.point(0, 0).toPoint();
+		//g.setSRID(4326);
+
+		data.add(Features.create(null, data.schema(),
+		                         g, 1, "anvil",
+		                         10.99));
+
+		data.add(Features.create(null, data.schema(),
+		                         gb.points(10, 10, 20, 20).toLineString(),
+		                         2, "bomb", 11.99));
+
+		data.add(Features.create(null, data.schema(),
+		                         gb.point(100, 10).toPoint().buffer(10),
+		                         3, "dynamite", 12.99));
+
+		//Dataset jsonData = new GeoJSONDataset(new File("states.json"));
+		//mem.put("states", jsonData);
+
+		try {
+			return mem.get(layer);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+
+		return null;
+	}
+}
diff --git a/vtm-jeo/src/org/oscim/theme/carto/MatcherFeature.java b/vtm-jeo/src/org/oscim/theme/carto/MatcherFeature.java
new file mode 100644
index 00000000..8e345db8
--- /dev/null
+++ b/vtm-jeo/src/org/oscim/theme/carto/MatcherFeature.java
@@ -0,0 +1,74 @@
+package org.oscim.theme.carto;
+
+import static java.lang.System.out;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jeo.feature.BasicFeature;
+import org.oscim.core.Tag;
+import org.oscim.core.TagSet;
+
+//imitate Feature behaviour for tags and zoom-level
+class MatcherFeature extends BasicFeature {
+	TagSet mTags;
+	Integer mZoom;
+
+	void setTags(TagSet tags) {
+		mTags = tags;
+	}
+
+	void setZoom(int zoom) {
+		mZoom = Integer.valueOf(zoom);
+	}
+
+	protected MatcherFeature() {
+		super("");
+	}
+
+	@Override
+	public Object get(String key) {
+		//out.println("get(" + key + ")");
+
+		if (key.equals("zoom"))
+			return mZoom;
+
+		Tag t = mTags.get(key.intern());
+		if (t == null)
+			return null;
+
+		//out.println("value: " + t.value);
+
+		return t.value;
+	}
+
+	@Override
+	public void put(String key, Object val) {
+		out.println("EEEK put()");
+	}
+
+	@Override
+	public List<Object> list() {
+		out.println("EEEK list()");
+		return null;
+	}
+
+	@Override
+	public Map<String, Object> map() {
+		out.println("EEEK map()");
+		return null;
+	}
+
+	@Override
+	public Object get(int arg0) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	@Override
+	public void set(int arg0, Object arg1) {
+		// TODO Auto-generated method stub
+
+	}
+
+};
diff --git a/vtm-jeo/src/org/oscim/theme/carto/RenderTheme.java b/vtm-jeo/src/org/oscim/theme/carto/RenderTheme.java
new file mode 100644
index 00000000..042a8794
--- /dev/null
+++ b/vtm-jeo/src/org/oscim/theme/carto/RenderTheme.java
@@ -0,0 +1,266 @@
+package org.oscim.theme.carto;
+
+import static java.lang.System.out;
+import static org.jeo.map.CartoCSS.BACKGROUND_COLOR;
+import static org.jeo.map.CartoCSS.OPACITY;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.jeo.carto.Carto;
+import org.jeo.map.CartoCSS;
+import org.jeo.map.RGB;
+import org.jeo.map.Rule;
+import org.jeo.map.RuleList;
+import org.jeo.map.Selector;
+import org.jeo.map.Style;
+import org.oscim.core.GeometryBuffer.GeometryType;
+import org.oscim.core.MapElement;
+import org.oscim.core.Tag;
+import org.oscim.core.TagSet;
+import org.oscim.theme.IRenderTheme;
+import org.oscim.theme.renderinstruction.Area;
+import org.oscim.theme.renderinstruction.Line;
+import org.oscim.theme.renderinstruction.RenderInstruction;
+
+public class RenderTheme implements IRenderTheme {
+
+	final String STYLE = "" +
+
+	        "[building = 'yes'] {" +
+	        " z: 1;" +
+	        "  polygon-fill: #eee;" +
+	        "  [zoom >= 16] {" +
+	        "    polygon-fill: #c00;" +
+	        "  }" +
+	        "}" +
+
+	        "[admin_level = '2'] {" +
+	        " line-color: #000;" +
+	        " line-width: 1;" +
+	        " z: 1;" +
+	        "}" +
+
+	        "[admin_level = '2'] {" +
+	        " line-color: #000;" +
+	        " line-width: 1;" +
+	        " z: 1;" +
+	        "}" +
+
+	        "[admin_level = '4'] {" +
+	        " line-color: #aaa;" +
+	        " line-width: 1;" +
+	        " z: 2;" +
+	        "}" +
+
+	        "[highway = 'motorway'] {" +
+	        " line-color: #a00;" +
+	        " z: 10;" +
+	        "}" +
+
+	        "[highway = 'primary'] {" +
+	        " line-color: #aa0;" +
+	        " z: 11;" +
+	        "}" +
+
+	        "[highway = 'residential'],[highway = 'road'],[highway = 'secondary'] {" +
+	        " line-color: #fff;" +
+	        " z: 12;" +
+	        "}" +
+
+	        " [landuse = 'forest'] {" +
+	        " polygon-fill: #0a0;" +
+	        " z: 2;" +
+	        "}" +
+
+	        "[natural = 'water'] {" +
+	        " polygon-fill: #00a;" +
+	        " z: 3;" +
+	        "}";
+
+	private Style mStyle;
+	private RuleList mRules;
+
+	MatcherFeature mMatchFeature = new MatcherFeature();
+	private int mBackground;
+
+	public RenderTheme() {
+
+		try {
+			mStyle = loadStyle();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+
+		// get map background
+		RuleList rules = mStyle.getRules().selectByName("Map", false);
+		if (!rules.isEmpty()) {
+			Rule rule = rules.collapse();
+			RGB bgColor = rule.color(null, BACKGROUND_COLOR, null);
+			if (bgColor != null) {
+				bgColor = bgColor.alpha(rule.number(null, OPACITY, 1f));
+				mBackground = color(bgColor);
+			}
+		}
+
+		mRules = mStyle.getRules();
+
+		//out.println(mRules);
+		//out.println();
+		if (mRules.get(1).equals(mRules.get(2)))
+			out.println("ok");
+
+		for (Rule r : mRules)
+			out.println(formatRule(r, 0));
+	}
+
+	class StyleSet {
+		int level;
+		RenderInstruction[] ri = new RenderInstruction[2];
+	}
+
+	Map<Rule, StyleSet> mStyleSets = new HashMap<Rule, StyleSet>();
+	int mCurLevel = 0;
+
+	public String formatRule(Rule r, int indent) {
+		StringBuilder sb = new StringBuilder();
+
+		String pad = "";
+		for (int i = 0; i < indent; i++)
+			pad += " ";
+
+		sb.append(pad);
+
+		for (Selector s : r.getSelectors()) {
+			sb.append(RuleDebug.formatSelector(s));
+			sb.append(",");
+		}
+
+		if (sb.length() > 0)
+			sb.setLength(sb.length() - 1);
+
+		sb.append(pad).append(" {").append("\n");
+
+		StyleSet s = new StyleSet();
+		RGB l = null;
+		RGB p = null;
+		if (r.properties().containsKey(CartoCSS.LINE_COLOR)) {
+			l = r.color(null, CartoCSS.LINE_COLOR, RGB.black);
+		}
+		if (r.properties().containsKey(CartoCSS.POLYGON_FILL)) {
+			p = r.color(null, CartoCSS.POLYGON_FILL, RGB.black);
+		}
+
+		if (p != null) {
+			s.ri[0] = new Area(mCurLevel++, color(p));
+		}
+
+		if (l != null) {
+			s.ri[1] = new Line(mCurLevel++, color(l), 1);
+		}
+
+		if (p != null || l != null) {
+			mStyleSets.put(r, s);
+			out.println("put " + s.ri[0] + s.ri[1]);
+		}
+
+		for (Map.Entry<String, Object> e : r.properties().entrySet()) {
+			sb.append(pad).append("  ").append(e.getKey()).append(": ").append(e.getValue())
+			    .append(";\n");
+		}
+
+		for (Rule nested : r.nested()) {
+			sb.append(formatRule(nested, indent + 2)).append("\n");
+		}
+
+		sb.append(pad).append("}");
+		return sb.toString();
+	}
+
+	Style loadStyle() throws IOException {
+		return Carto.parse(STYLE);
+	}
+
+	@Override
+	public synchronized RenderInstruction[] matchElement(GeometryType type, TagSet tags,
+	        int zoomLevel) {
+		MatcherFeature f = mMatchFeature;
+
+		f.setTags(tags);
+		f.setZoom(zoomLevel);
+
+		RuleList rules = mRules.match(f);
+
+		Rule r = rules.collapse();
+
+		//out.println(r);
+		if (rules.isEmpty())
+			return null;
+
+		int z = r.number(f, "z", 0f).intValue();
+
+		if (type == GeometryType.POLY) {
+			RGB c = r.color(f, CartoCSS.POLYGON_FILL, RGB.black);
+			out.println(z + " " + c);
+			return new RenderInstruction[] {
+			        new Area(z, color(c))
+			};
+
+		} else if (type == GeometryType.LINE) {
+			RGB c = r.color(f, CartoCSS.LINE_COLOR, RGB.black);
+			float width = r.number(f, CartoCSS.LINE_WIDTH, 2f);
+			//out.println(z + " " + c);
+
+			return new RenderInstruction[] {
+			        new Line(100 + z, color(c), width)
+			};
+
+		} else if (type == GeometryType.POINT) {
+			//RGB c = r.color(f, CartoCSS.MARKER_FILL, RGB.black);
+			//out.println(c);
+			//return new RenderInstruction[] {
+			//		new Caption(color(c), width)
+			//};
+		}
+
+		return null;
+	}
+
+	public static int color(RGB rgb) {
+		return rgb.getAlpha() << 24
+		        | rgb.getRed() << 16
+		        | rgb.getGreen() << 8
+		        | rgb.getBlue();
+	}
+
+	@Override
+	public void destroy() {
+	}
+
+	@Override
+	public int getLevels() {
+		return 1;
+	}
+
+	@Override
+	public int getMapBackground() {
+		return mBackground;
+	}
+
+	@Override
+	public void scaleTextSize(float scaleFactor) {
+	}
+
+	public static void main(String[] args) {
+		RenderTheme t = new RenderTheme();
+
+		MapElement e = new MapElement();
+		e.startPolygon();
+		e.tags.add(new Tag("building", "yes"));
+
+		t.matchElement(GeometryType.POLY, e.tags, 16);
+		t.matchElement(GeometryType.POLY, e.tags, 15);
+	}
+
+}
diff --git a/vtm-jeo/src/org/oscim/theme/carto/RuleDebug.java b/vtm-jeo/src/org/oscim/theme/carto/RuleDebug.java
new file mode 100644
index 00000000..ebc92865
--- /dev/null
+++ b/vtm-jeo/src/org/oscim/theme/carto/RuleDebug.java
@@ -0,0 +1,74 @@
+package org.oscim.theme.carto;
+
+import static java.lang.System.out;
+
+import java.util.Map;
+
+import org.jeo.filter.Filter;
+import org.jeo.map.Rule;
+import org.jeo.map.Selector;
+
+public class RuleDebug {
+
+	static void printRule(Rule r, int level) {
+
+		out.println("> " + level + " >");
+		out.println(formatRule(r, level));
+	}
+
+	public static String formatRule(Rule r, int indent) {
+		StringBuilder sb = new StringBuilder();
+		String pad = "";
+		for (int i = 0; i < indent; i++) {
+			pad += " ";
+		};
+
+		sb.append(pad);
+		for (Selector s : r.getSelectors()) {
+			sb.append(formatSelector(s));
+			sb.append(",");
+		}
+		if (sb.length() > 0) {
+			sb.setLength(sb.length() - 1);
+		}
+		sb.append(pad).append(" {").append("\n");
+
+		for (Map.Entry<String, Object> e : r.properties().entrySet()) {
+			sb.append(pad).append("  ").append(e.getKey()).append(": ").append(e.getValue())
+					.append(";\n");
+		}
+
+		for (Rule nested : r.nested()) {
+			sb.append(nested.toString(indent + 2)).append("\n");
+		}
+
+		sb.append(pad).append("}");
+		return sb.toString();
+	}
+
+	public static String formatSelector(Selector s) {
+		StringBuffer sb = new StringBuffer();
+
+		if (s.getName() != null) {
+			sb.append(s.getName());
+		}
+		if (s.getId() != null) {
+			sb.append("#").append(s.getId());
+		}
+		for (String c : s.getClasses()) {
+			sb.append(".").append(c);
+		}
+		if (s.getFilter() != null && s.getFilter() != Filter.TRUE) {
+			sb.append("[").append(s.getFilter()).append("]");
+		}
+		if (s.getAttachment() != null) {
+			sb.append("::").append(s.getAttachment());
+		}
+
+		if (s.isWildcard()) {
+			sb.append("*");
+		}
+
+		return sb.toString();
+	}
+}