From 6de30b63b863b203ef5e67d0848aa6b651cef6e5 Mon Sep 17 00:00:00 2001 From: David Winslow Date: Thu, 12 Apr 2012 10:26:18 -0400 Subject: [PATCH 1/6] Refactor the rule grouping code a bit --- geocss/src/main/scala/Translator.scala | 42 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/geocss/src/main/scala/Translator.scala b/geocss/src/main/scala/Translator.scala index 42d332a..01ee016 100644 --- a/geocss/src/main/scala/Translator.scala +++ b/geocss/src/main/scala/Translator.scala @@ -607,12 +607,13 @@ 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) + val zGroups: Seq[(Double, (Rule, Seq[gt.Symbolizer]))] = + for { + rule <- cascading2exclusive(overlays) + (z, syms) <- groupByZ(symbolize(rule)) + } yield (z, (rule, syms)) - for ((_, group) <- flattenByZ(zGroups.flatten)) { + for ((_, group) <- orderedRuns(zGroups)) { val fts = styles.createFeatureTypeStyle typename.foreach { t => fts.featureTypeNames.add(new NameImpl(t)) } for ((rule, syms) <- group if !syms.isEmpty) { @@ -656,13 +657,34 @@ 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) + /** + * Order-preserving grouping of pairs by the first item in the tuple + * "runs" as in run-length encoding. + */ + 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) + } + + 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)) private def groupByZ(syms: Seq[(Double, Symbolizer)]) : Seq[(Double, Seq[Symbolizer])] = { From 26d7125fcb4643145573bc5a78960547db56ae91 Mon Sep 17 00:00:00 2001 From: David Winslow Date: Thu, 12 Apr 2012 10:43:03 -0400 Subject: [PATCH 2/6] Lay some groundwork for outputting transformation expressions --- geocss/src/main/scala/Translator.scala | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/geocss/src/main/scala/Translator.scala b/geocss/src/main/scala/Translator.scala index 01ee016..c88faf2 100644 --- a/geocss/src/main/scala/Translator.scala +++ b/geocss/src/main/scala/Translator.scala @@ -607,14 +607,24 @@ 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[(Double, (Rule, Seq[gt.Symbolizer]))] = + val zGroups: Seq[((Double, Option[OGCExpression]), (Rule, Seq[gt.Symbolizer]))] = for { rule <- cascading2exclusive(overlays) (z, syms) <- groupByZ(symbolize(rule)) - } yield (z, (rule, syms)) - - for ((_, group) <- orderedRuns(zGroups)) { + } yield ((z, None), (rule, syms)) + + // 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 = zGroups.map { case ((_, tx), _) => tx }.distinct + implicit val transformOrdering: Ordering[OGCExpression] = + Ordering.by { x => allTransforms.indexOf(x) } + + 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() From 573bd05326192b5faa9b5a2e379951922c6b8344 Mon Sep 17 00:00:00 2001 From: David Winslow Date: Thu, 12 Apr 2012 10:43:21 -0400 Subject: [PATCH 3/6] Better scaladoc note for the "runs" function --- geocss/src/main/scala/Translator.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geocss/src/main/scala/Translator.scala b/geocss/src/main/scala/Translator.scala index c88faf2..35b5f41 100644 --- a/geocss/src/main/scala/Translator.scala +++ b/geocss/src/main/scala/Translator.scala @@ -668,8 +668,8 @@ class Translator(val baseURL: Option[java.net.URL]) { } /** - * Order-preserving grouping of pairs by the first item in the tuple - * "runs" as in run-length encoding. + * "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)] From 2e16ae4f26d47490b8e0cdd94d8dd76ba5cdb105 Mon Sep 17 00:00:00 2001 From: David Winslow Date: Thu, 12 Apr 2012 10:47:04 -0400 Subject: [PATCH 4/6] More refactoring --- geocss/src/main/scala/Translator.scala | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/geocss/src/main/scala/Translator.scala b/geocss/src/main/scala/Translator.scala index 35b5f41..fa68f58 100644 --- a/geocss/src/main/scala/Translator.scala +++ b/geocss/src/main/scala/Translator.scala @@ -610,7 +610,7 @@ class Translator(val baseURL: Option[java.net.URL]) { val zGroups: Seq[((Double, Option[OGCExpression]), (Rule, Seq[gt.Symbolizer]))] = for { rule <- cascading2exclusive(overlays) - (z, syms) <- groupByZ(symbolize(rule)) + (z, syms) <- orderedRuns(symbolize(rule)) } yield ((z, None), (rule, syms)) // In order to ensure minimal output, the conversion requires that like @@ -696,18 +696,6 @@ class Translator(val baseURL: Option[java.net.URL]) { private def orderedRuns[K : Ordering, V] (xs: Seq[(K, V)]) : Seq[(K, Seq[V])] = runs(xs sortBy(_._1)) - 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))) - } - /** * Given a list, generate all possible groupings of the contents of that list * into two sublists. The sublists preserve the ordering of the original From 4186ff907744e31a0f6e7c9fe4c05e50e1879719 Mon Sep 17 00:00:00 2001 From: David Winslow Date: Thu, 12 Apr 2012 11:02:37 -0400 Subject: [PATCH 5/6] Set up symbolize() to extract transformation properties from rules --- geocss/src/main/scala/Translator.scala | 39 ++++++++++++++++---------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/geocss/src/main/scala/Translator.scala b/geocss/src/main/scala/Translator.scala index fa68f58..b590da3 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) => @@ -388,10 +389,10 @@ class Translator(val baseURL: Option[java.net.URL]) { null ) sym.setGeometry(geom) - (zIndex, sym) + ((zIndex, None), 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) => @@ -419,10 +420,10 @@ class Translator(val baseURL: Option[java.net.URL]) { null ) sym.setGeometry(geom) - (zIndex, sym) + ((zIndex, None), 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) => @@ -438,11 +439,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, None), 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) => @@ -549,7 +550,7 @@ class Translator(val baseURL: Option[java.net.URL]) { ) } - (zIndex, sym) + ((zIndex, None), sym) } Seq(polySyms, lineSyms, pointSyms, textSyms).flatten @@ -607,21 +608,29 @@ 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[((Double, Option[OGCExpression]), (Rule, Seq[gt.Symbolizer]))] = - for { - rule <- cascading2exclusive(overlays) - (z, syms) <- orderedRuns(symbolize(rule)) - } yield ((z, None), (rule, syms)) + 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 = zGroups.map { case ((_, tx), _) => tx }.distinct + 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 } From ea8885eaef1b9dee00c5e4527faac672cdd4461a Mon Sep 17 00:00:00 2001 From: David Winslow Date: Thu, 12 Apr 2012 14:12:08 -0400 Subject: [PATCH 6/6] Extract transformations from CSS properties --- geocss/src/main/scala/Translator.scala | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/geocss/src/main/scala/Translator.scala b/geocss/src/main/scala/Translator.scala index b590da3..10532b7 100644 --- a/geocss/src/main/scala/Translator.scala +++ b/geocss/src/main/scala/Translator.scala @@ -361,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 @@ -389,7 +391,7 @@ class Translator(val baseURL: Option[java.net.URL]) { null ) sym.setGeometry(geom) - ((zIndex, None), sym) + ((zIndex, transform), sym) } val polySyms: Seq[(Key, PolygonSymbolizer)] = @@ -402,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 @@ -420,7 +424,7 @@ class Translator(val baseURL: Option[java.net.URL]) { null ) sym.setGeometry(geom) - ((zIndex, None), sym) + ((zIndex, transform), sym) } val pointSyms: Seq[(Key, PointSymbolizer)] = @@ -429,6 +433,8 @@ class Translator(val baseURL: Option[java.net.URL]) { ).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 @@ -439,7 +445,7 @@ class Translator(val baseURL: Option[java.net.URL]) { for (g <- graphic) yield { val sym = styles.createPointSymbolizer(g, null) sym.setGeometry(geom) - ((zIndex, None), sym) + ((zIndex, transform), sym) } } @@ -455,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 @@ -550,7 +558,7 @@ class Translator(val baseURL: Option[java.net.URL]) { ) } - ((zIndex, None), sym) + ((zIndex, transform), sym) } Seq(polySyms, lineSyms, pointSyms, textSyms).flatten