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 list() { + out.println("EEEK list()"); + return null; + } + + @Override + public Map 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 mStyleSets = new HashMap(); + 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 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 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(); + } +}