Skip to content

Commit 2312034

Browse files
committed
Add a legend to carto documents.
1 parent 155035c commit 2312034

12 files changed

+794
-12
lines changed

src/main/groovy/geoscript/carto/CartoBuilder.groovy

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ interface CartoBuilder {
9090
*/
9191
CartoBuilder table(TableItem tableItem)
9292

93+
/**
94+
* Add a legend
95+
* @param legendItem The LegendItem
96+
* @return The CartoBuilder
97+
*/
98+
CartoBuilder legend(LegendItem legendItem)
99+
93100
/**
94101
* Write the cartographic document to the OutputStream
95102
* @param outputStream The OutputStream

src/main/groovy/geoscript/carto/ImageCartoBuilder.groovy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ class ImageCartoBuilder implements CartoBuilder {
105105
this
106106
}
107107

108+
@Override
109+
CartoBuilder legend(LegendItem legendItem) {
110+
java2DCartoBuilder.legend(legendItem)
111+
this
112+
}
113+
108114
@Override
109115
void build(OutputStream outputStream) {
110116
java2DCartoBuilder.graphics.dispose()

src/main/groovy/geoscript/carto/Java2DCartoBuilder.groovy

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package geoscript.carto
22

33
import geoscript.feature.Schema
4+
import geoscript.geom.Bounds
45
import geoscript.geom.Geometry
6+
import geoscript.geom.LineString
57
import geoscript.layer.Layer
68
import geoscript.proj.Projection
79
import geoscript.render.Map
10+
import geoscript.style.ColorMap
811
import geoscript.workspace.Memory
912

1013
import javax.imageio.ImageIO
@@ -20,6 +23,7 @@ import java.awt.font.TextAttribute
2023
import java.awt.geom.GeneralPath
2124
import java.text.AttributedString
2225
import java.text.DecimalFormat
26+
import java.text.NumberFormat
2327
import java.text.SimpleDateFormat
2428

2529
/**
@@ -122,7 +126,7 @@ class Java2DCartoBuilder implements CartoBuilder {
122126
FontMetrics fontMetrics = graphics.fontMetrics
123127
String text = "N"
124128
int textHeight = fontMetrics.height
125-
drawString(text, new Rectangle(0, height - textHeight, width, textHeight), HorizontalAlign.CENTER, VerticalAlign.BOTTOM)
129+
drawString(text, new Rectangle(x, y + (height - textHeight), width, textHeight), HorizontalAlign.CENTER, VerticalAlign.BOTTOM)
126130
height = height - textHeight
127131
}
128132

@@ -164,10 +168,10 @@ class Java2DCartoBuilder implements CartoBuilder {
164168
FontMetrics fontMetrics = graphics.fontMetrics
165169
int textWidth = [fontMetrics.stringWidth("N"), fontMetrics.stringWidth("E"), fontMetrics.stringWidth("S"), fontMetrics.stringWidth("W")].max()
166170
int textHeight = fontMetrics.height
167-
drawString("N", new Rectangle((width / 2 - textWidth / 2) as int, 0, textWidth, textHeight), HorizontalAlign.CENTER, VerticalAlign.TOP)
168-
drawString("E", new Rectangle(width - textWidth, (height / 2 - textHeight / 2) as int, textWidth, textHeight), HorizontalAlign.RIGHT, VerticalAlign.MIDDLE)
169-
drawString("S", new Rectangle((width / 2 - textWidth / 2) as int, height - textHeight, textWidth, textHeight), HorizontalAlign.CENTER, VerticalAlign.TOP)
170-
drawString("W", new Rectangle(0, (height / 2 - textHeight / 2) as int, textWidth, textHeight), HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
171+
drawString("N", new Rectangle(x + (width / 2 - textWidth / 2) as int, y, textWidth, textHeight), HorizontalAlign.CENTER, VerticalAlign.TOP)
172+
drawString("E", new Rectangle(x + (width - textWidth), y + (height / 2 - textHeight / 2) as int, textWidth, textHeight), HorizontalAlign.RIGHT, VerticalAlign.MIDDLE)
173+
drawString("S", new Rectangle(x + (width / 2 - textWidth / 2) as int, y + (height - textHeight), textWidth, textHeight), HorizontalAlign.CENTER, VerticalAlign.TOP)
174+
drawString("W", new Rectangle(x, y + (height / 2 - textHeight / 2) as int, textWidth, textHeight), HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
171175
x = x + textWidth
172176
y = y + textHeight
173177
width = width - (textWidth * 2)
@@ -398,6 +402,170 @@ class Java2DCartoBuilder implements CartoBuilder {
398402
this
399403
}
400404

405+
@Override
406+
CartoBuilder legend(LegendItem legendItem) {
407+
408+
// Draw background
409+
if(legendItem.backgroundColor) {
410+
graphics.color = legendItem.backgroundColor
411+
graphics.fillRect(legendItem.x, legendItem.y, legendItem.width, legendItem.height)
412+
}
413+
414+
// Draw title
415+
graphics.font = legendItem.titleFont
416+
graphics.color = legendItem.titleColor
417+
FontMetrics fm = graphics.fontMetrics
418+
int titleHeight = fm.height
419+
drawString(legendItem.title, new Rectangle(legendItem.x, legendItem.y, legendItem.width, titleHeight), HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
420+
421+
// Draw Entries
422+
graphics.font = legendItem.textFont
423+
graphics.color = legendItem.textColor
424+
fm = graphics.fontMetrics
425+
426+
// Keep track of the entry x and y
427+
int entryX = legendItem.x
428+
int entryY = legendItem.y + titleHeight + legendItem.gapBetweenEntries
429+
int maxTextWidth = -1
430+
431+
legendItem.entries.eachWithIndex { LegendItem.LegendEntry entry, int i ->
432+
433+
if (entry.type == LegendItem.LegendEntryType.COLORMAP) {
434+
435+
// Draw title
436+
graphics.color = legendItem.textColor
437+
Rectangle titleRectangle = new Rectangle(entryX, entryY, legendItem.legendEntryWidth * 2, fm.height)
438+
drawString(entry.title, titleRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
439+
entryY += fm.height + legendItem.gapBetweenEntries
440+
441+
NumberFormat numberFormat = new DecimalFormat(legendItem.numberFormat)
442+
ColorMap colorMap = entry.symbolizer as ColorMap
443+
colorMap.values.each { java.util.Map value ->
444+
445+
String title = numberFormat.format(value.quantity)
446+
String colorHex = "${value.color}"
447+
448+
Rectangle symbolRectangle = new Rectangle(
449+
entryX,
450+
entryY,
451+
legendItem.legendEntryWidth,
452+
legendItem.legendEntryHeight
453+
)
454+
graphics.color = Color.decode(colorHex)
455+
graphics.fill(symbolRectangle)
456+
457+
Rectangle textRectangle = new Rectangle(
458+
entryX + legendItem.legendEntryWidth + legendItem.gapBetweenEntries,
459+
entryY,
460+
legendItem.legendEntryWidth,
461+
legendItem.legendEntryHeight
462+
)
463+
maxTextWidth = Math.max(maxTextWidth, fm.stringWidth(title) + legendItem.gapBetweenEntries)
464+
graphics.color = legendItem.textColor
465+
drawString(title, textRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
466+
467+
entryY += legendItem.legendEntryHeight
468+
}
469+
470+
entryY += legendItem.gapBetweenEntries
471+
472+
} else if (entry.type == LegendItem.LegendEntryType.IMAGE) {
473+
474+
// Draw Image
475+
Rectangle imageRectangle = new Rectangle(entryX, entryY, legendItem.legendEntryWidth, legendItem.legendEntryHeight)
476+
graphics.drawImage(entry.image, imageRectangle.x as int, imageRectangle.y as int, imageRectangle.width as int, imageRectangle.height as int, null)
477+
478+
// Draw title
479+
graphics.color = legendItem.textColor
480+
Rectangle titleRectangle = new Rectangle(
481+
entryX + legendItem.legendEntryWidth + legendItem.gapBetweenEntries,
482+
entryY,
483+
legendItem.legendEntryWidth,
484+
legendItem.legendEntryHeight
485+
)
486+
drawString(entry.title, titleRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
487+
488+
entryY += legendItem.legendEntryHeight + legendItem.gapBetweenEntries
489+
490+
} else if (entry.type == LegendItem.LegendEntryType.GROUP) {
491+
492+
// Draw title
493+
graphics.color = legendItem.textColor
494+
Rectangle titleRectangle = new Rectangle(entryX, entryY, legendItem.legendEntryWidth * 2, fm.height)
495+
drawString(entry.title, titleRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
496+
entryY += fm.height + legendItem.gapBetweenEntries
497+
498+
} else /* POINT, LINE, POLYGON */ {
499+
500+
Rectangle symbolRectangle = new Rectangle(
501+
entryX,
502+
entryY,
503+
legendItem.legendEntryWidth,
504+
legendItem.legendEntryHeight
505+
)
506+
507+
Geometry geometry
508+
if (entry.type == LegendItem.LegendEntryType.POLYGON) {
509+
geometry = new Bounds(2, 2, legendItem.legendEntryWidth - 2, legendItem.legendEntryHeight - 2).geometry
510+
} else if (entry.type == LegendItem.LegendEntryType.LINE) {
511+
geometry = new LineString([[0, legendItem.legendEntryHeight / 2], [legendItem.legendEntryWidth, legendItem.legendEntryHeight / 2]])
512+
} else if (entry.type == LegendItem.LegendEntryType.POINT) {
513+
geometry = new geoscript.geom.Point((legendItem.legendEntryWidth / 2) as int, (legendItem.legendEntryHeight / 2) as int)
514+
}
515+
516+
Layer layer = Layer.fromGeometry("polygon", geometry, style: entry.symbolizer)
517+
Map map = new Map(
518+
width: legendItem.legendEntryWidth,
519+
height: legendItem.legendEntryHeight,
520+
fixAspectRatio: false,
521+
bounds: new Bounds(0, 0, legendItem.legendEntryWidth, legendItem.legendEntryHeight),
522+
layers: [layer]
523+
)
524+
graphics.drawImage(map.renderToImage(), symbolRectangle.x as int, symbolRectangle.y as int, null)
525+
526+
Rectangle textRectangle = new Rectangle(
527+
entryX + legendItem.legendEntryWidth + legendItem.gapBetweenEntries,
528+
entryY,
529+
legendItem.legendEntryWidth,
530+
legendItem.legendEntryHeight
531+
)
532+
maxTextWidth = Math.max(maxTextWidth, fm.stringWidth(entry.title) + legendItem.gapBetweenEntries)
533+
drawString(entry.title, textRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
534+
535+
entryY += legendItem.legendEntryHeight + legendItem.gapBetweenEntries
536+
}
537+
538+
// Are there more entries?
539+
if (i < (legendItem.entries.size() - 1)) {
540+
// Check to make sure the next entry can fit in the remaining space
541+
int nextHeight = getLegendItemHeight(legendItem, legendItem.entries[i + 1])
542+
if ((entryY + nextHeight) > legendItem.height) {
543+
// Move the entry x over to the right to a separate column
544+
entryX += legendItem.legendEntryWidth + legendItem.gapBetweenEntries + maxTextWidth
545+
// Reset the entry y to the top
546+
entryY = legendItem.y + titleHeight + legendItem.gapBetweenEntries
547+
// Reset the max text width
548+
maxTextWidth = -1
549+
}
550+
}
551+
}
552+
this
553+
}
554+
555+
private int getLegendItemHeight(LegendItem item, LegendItem.LegendEntry entry) {
556+
int height
557+
if (entry.type == LegendItem.LegendEntryType.COLORMAP) {
558+
graphics.font = item.textFont
559+
height = graphics.fontMetrics.height + item.gapBetweenEntries + (entry.symbolizer.values.size() * item.legendEntryHeight) + item.gapBetweenEntries
560+
} else if (entry.type == LegendItem.LegendEntryType.GROUP) {
561+
graphics.font = item.textFont
562+
height = graphics.fontMetrics.height + item.gapBetweenEntries
563+
} else {
564+
height = item.legendEntryHeight + item.gapBetweenEntries
565+
}
566+
height
567+
}
568+
401569
private void drawString(String text, Rectangle rectangle, HorizontalAlign horizontalAlign, VerticalAlign verticalAlign) {
402570
drawString(text, rectangle, horizontalAlign, verticalAlign, false)
403571
}

0 commit comments

Comments
 (0)