diff --git a/geocss/src/main/scala/Translator.scala b/geocss/src/main/scala/Translator.scala index 42d332a..10532b7 100644 --- a/geocss/src/main/scala/Translator.scala +++ b/geocss/src/main/scala/Translator.scala @@ -338,13 +338,14 @@ class Translator(val baseURL: Option[java.net.URL]) { * Convert a set of properties to a set of Symbolizer objects attached to the * given Rule. */ - def symbolize(rule: Rule): Seq[Pair[Double, Symbolizer]] = { + def symbolize(rule: Rule): Seq[Pair[(Double, Option[OGCExpression]), Symbolizer]] = { + type Key = (Double, Option[OGCExpression]) val properties = rule.properties def orderedMarkRules(symbolizerType: String, order: Int): Seq[Property] = rule.context(symbolizerType, order) - val lineSyms: Seq[(Double, LineSymbolizer)] = + val lineSyms: Seq[(Key, LineSymbolizer)] = (expand(properties, "stroke").toStream zip (Stream.from(1) map { orderedMarkRules("stroke", _) }) ).map { case (props, markProps) => @@ -360,6 +361,8 @@ class Translator(val baseURL: Option[java.net.URL]) { val rotation = props.get("stroke-rotation") map angle getOrElse filters.literal(0) val geom = props.get("stroke-geometry") orElse props.get("geometry") map expression getOrElse null + val transform = + props.get("stroke-transform") orElse props.get("transform") map expression val zIndex: Double = props.get("stroke-z-index") orElse props.get("z-index") map { x => keyword("0", x).toDouble @@ -388,10 +391,10 @@ class Translator(val baseURL: Option[java.net.URL]) { null ) sym.setGeometry(geom) - (zIndex, sym) + ((zIndex, transform), sym) } - val polySyms: Seq[(Double, PolygonSymbolizer)] = + val polySyms: Seq[(Key, PolygonSymbolizer)] = (expand(properties, "fill").toStream zip (Stream.from(1) map { orderedMarkRules("fill", _) }) ).map { case (props, markProps) => @@ -401,6 +404,8 @@ class Translator(val baseURL: Option[java.net.URL]) { val opacity = props.get("fill-opacity") map scale getOrElse null val geom = props.get("fill-geometry") orElse props.get("geometry") map expression getOrElse null + val transform = + props.get("fill-transform") orElse props.get("transform") map expression val zIndex: Double = props.get("fill-z-index") orElse props.get("z-index") map { x => keyword("0", x).toDouble @@ -419,15 +424,17 @@ class Translator(val baseURL: Option[java.net.URL]) { null ) sym.setGeometry(geom) - (zIndex, sym) + ((zIndex, transform), sym) } - val pointSyms: Seq[(Double, PointSymbolizer)] = + val pointSyms: Seq[(Key, PointSymbolizer)] = (expand(properties, "mark").toStream zip (Stream.from(1) map { orderedMarkRules("mark", _) }) ).flatMap { case (props, markProps) => val geom = (props.get("mark-geometry") orElse props.get("geometry")) .map(expression).getOrElse(null) + val transform = + props.get("mark-transform") orElse props.get("transform") map expression val zIndex: Double = props.get("mark-z-index") orElse props.get("z-index") map { x => keyword(x).toDouble @@ -438,11 +445,11 @@ class Translator(val baseURL: Option[java.net.URL]) { for (g <- graphic) yield { val sym = styles.createPointSymbolizer(g, null) sym.setGeometry(geom) - (zIndex, sym) + ((zIndex, transform), sym) } } - val textSyms: Seq[(Double, TextSymbolizer)] = + val textSyms: Seq[(Key, TextSymbolizer)] = (expand(properties, "label").toStream zip (Stream.from(1) map { orderedMarkRules("shield", _) }) ).map { case (props, shieldProps) => @@ -454,6 +461,8 @@ class Translator(val baseURL: Option[java.net.URL]) { val rotation = props.get("label-rotation").map(angle) val geom = (props.get("label-geometry") orElse props.get("geometry")) .map(expression).getOrElse(null) + val transform = + props.get("label-transform") orElse props.get("transform") map expression val zIndex: Double = props.get("label-z-index") orElse props.get("z-index") map { x => keyword("0", x).toDouble @@ -549,7 +558,7 @@ class Translator(val baseURL: Option[java.net.URL]) { ) } - (zIndex, sym) + ((zIndex, transform), sym) } Seq(polySyms, lineSyms, pointSyms, textSyms).flatten @@ -607,13 +616,32 @@ class Translator(val baseURL: Option[java.net.URL]) { for (name <- typenames) yield (name, rules filter isForTypename(name) map stripTypenames) for ((typename, overlays) <- styleRules) { - val zGroups: Seq[Seq[(Double, Rule, Seq[gt.Symbolizer])]] = - for (rule <- cascading2exclusive(overlays)) yield - for ((z, syms) <- groupByZ(symbolize(rule))) yield - (z, rule, syms) - - for ((_, group) <- flattenByZ(zGroups.flatten)) { + val expandedRules = cascading2exclusive(overlays) + + // In order to ensure minimal output, the conversion requires that like + // transforms be sorted together. However, there is no natural ordering + // over OGC Expressions. Instead, we synthesize one by generating a list + // of all transform expressions used in this stylesheet and indexing into + // it to get a sort key. + val allTransforms = + expandedRules + .flatMap(symbolize) + .map { case ((_, tx), _) => tx } + .distinct + + implicit val transformOrdering: Ordering[OGCExpression] = + Ordering.by { x => allTransforms.indexOf(x) } + + val zGroups: Seq[((Double, Option[OGCExpression]), (Rule, Seq[gt.Symbolizer]))] = + for { + rule <- expandedRules + (key, syms) <- orderedRuns(symbolize(rule)) + } yield (key, (rule, syms)) + + + for (((_, transform), group) <- orderedRuns(zGroups)) { val fts = styles.createFeatureTypeStyle + transform.foreach { fts.setTransformation } typename.foreach { t => fts.featureTypeNames.add(new NameImpl(t)) } for ((rule, syms) <- group if !syms.isEmpty) { val sldRule = styles.createRule() @@ -656,26 +684,35 @@ class Translator(val baseURL: Option[java.net.URL]) { return sld } - private def flattenByZ[R, S](zGroups: Seq[(Double, R, Seq[S])]) - : Seq[(Double, Seq[(R, Seq[S])])] - = { - val zFlattened = zGroups map { case (z, r, s) => (z, (r, s)) } - (zFlattened groupBy(_._1) mapValues(_ map (_._2)) toSeq).sortBy(_._1) - } - + /** + * "runs" as in run-length encoding: Order-preserving grouping of pairs by + * the first item in the tuple + */ + private def runs[K, V](xs: Seq[(K,V)]): Seq[(K,Seq[V])] = { + type In = Seq[(K,V)] + type Out = Seq[(K,Seq[V])] + + @annotation.tailrec + def recurse(xs: In, accum: Out): Out = + xs match { + case Seq() => accum + case Seq(x, _*) => + val (in, out) = xs.span(_._1 == x._1) + val in0 = (x._1, (in map (_._2))) + recurse(out, accum :+ in0) + } - private def groupByZ(syms: Seq[(Double, Symbolizer)]) - : Seq[(Double, Seq[Symbolizer])] = { - // we make a special case for labels; they will be rendered last anyway, so - // we can fold them into one layer - val (labels, symbols) = syms partition { _.isInstanceOf[TextSymbolizer] } - val grouped = - for { - (z, syms) <- symbols.groupBy(_._1).toSeq.sortBy(_._1) - } yield (z, syms map (_._2)) - grouped ++ Seq((0d, labels map (_._2))) + recurse(xs, Seq.empty) } + /** + * Sort, then find runs in the sorted results + * + * @see runs + */ + private def orderedRuns[K : Ordering, V] (xs: Seq[(K, V)]) : Seq[(K, Seq[V])] = + runs(xs sortBy(_._1)) + /** * Given a list, generate all possible groupings of the contents of that list * into two sublists. The sublists preserve the ordering of the original