Skip to content

Commit 8f17f8a

Browse files
committed
Add scale bar
1 parent 624f6bc commit 8f17f8a

11 files changed

Lines changed: 363 additions & 4 deletions

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ interface CartoBuilder {
7676
*/
7777
CartoBuilder scaleText(ScaleTextItem scaleTextItem)
7878

79+
/**
80+
* Add scale bar
81+
* @param scaleBarItem The ScaleBarItem
82+
* @return The CartoBuilder
83+
*/
84+
CartoBuilder scaleBar(ScaleBarItem scaleBarItem)
85+
7986
/**
8087
* Add a grid (usually for visually placing other items)
8188
* @param gridItem The GridItem

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ class ImageCartoBuilder implements CartoBuilder {
8787
this
8888
}
8989

90+
@Override
91+
CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {
92+
java2DCartoBuilder.scaleBar(scaleBarItem)
93+
this
94+
}
95+
9096
@Override
9197
CartoBuilder grid(GridItem gridItem) {
9298
java2DCartoBuilder.grid(gridItem)

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,56 @@ class Java2DCartoBuilder implements CartoBuilder {
339339
this
340340
}
341341

342+
@Override
343+
CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {
344+
345+
int x = scaleBarItem.x
346+
int y = scaleBarItem.y
347+
int width = scaleBarItem.width
348+
int height = scaleBarItem.height
349+
int border = 5
350+
Font font = scaleBarItem.font
351+
Color strokeColor = scaleBarItem.strokeColor
352+
Color fillColor = scaleBarItem.fillColor
353+
int strokeWidth = scaleBarItem.strokeWidth
354+
int ticHeight = 10
355+
356+
ScaleBarItem.ScaleBarInfo scaleBarInfo = scaleBarItem.calculateScaleBarInfo()
357+
358+
// Check if the scalebar width matches the item width
359+
int padding = 0
360+
if (scaleBarInfo.widthInPixels == scaleBarItem.width) {
361+
padding = 5
362+
}
363+
364+
Rectangle r = new Rectangle(x,y,width,height)
365+
int lineXStart = (int)((r.x + r.width / 2) - scaleBarInfo.widthInPixels / 2) + padding
366+
int lineXEnd = (int)(lineXStart + scaleBarInfo.widthInPixels)
367+
int lineY = y + height - border
368+
369+
if (fillColor) {
370+
graphics.color = fillColor
371+
graphics.fillRect(lineXStart - border, y, lineXEnd - lineXStart + border * 2, height)
372+
}
373+
graphics.color = strokeColor
374+
graphics.stroke = new BasicStroke(strokeWidth)
375+
graphics.drawRect(lineXStart - border, y, lineXEnd - lineXStart + border * 2, height)
376+
377+
graphics.drawLine(lineXStart, lineY, lineXEnd, lineY)
378+
graphics.drawLine(lineXStart, y + height - border, lineXStart, y + height - border - ticHeight)
379+
graphics.drawLine(lineXEnd, y + height - border, lineXEnd, y + height - border - ticHeight)
380+
graphics.font = font
381+
String scaleText = "${(int) scaleBarInfo.widthInUnits} ${scaleBarInfo.unitForScaleText}"
382+
drawString(scaleText, new Rectangle(
383+
lineXStart,
384+
y + border,
385+
(int) scaleBarInfo.widthInPixels,
386+
height - border * 2
387+
), HorizontalAlign.CENTER, VerticalAlign.MIDDLE)
388+
389+
this
390+
}
391+
342392
@Override
343393
CartoBuilder table(TableItem tableItem) {
344394

src/main/groovy/geoscript/carto/PdfCartoBuilder.groovy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ class PdfCartoBuilder implements CartoBuilder {
9595
this
9696
}
9797

98+
@Override
99+
CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {
100+
java2DCartoBuilder.scaleBar(scaleBarItem)
101+
this
102+
}
103+
98104
@Override
99105
CartoBuilder grid(GridItem gridItem) {
100106
java2DCartoBuilder.grid(gridItem)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package geoscript.carto
2+
3+
import geoscript.render.Map
4+
import org.geotools.renderer.lite.RendererUtilities
5+
6+
import java.awt.Color
7+
import java.awt.Font
8+
9+
/**
10+
* Adds scale bar to a cartographic document.
11+
* @author Jared Erickson
12+
*/
13+
class ScaleBarItem extends Item {
14+
15+
Map map
16+
17+
Color strokeColor = Color.BLACK
18+
19+
Color fillColor = Color.WHITE
20+
21+
float strokeWidth = 1
22+
23+
Font font = new Font("Arial", Font.PLAIN, 12)
24+
25+
int border = 5
26+
27+
Units units = Units.METRIC
28+
29+
enum Units {
30+
METRIC,
31+
US
32+
}
33+
34+
/**
35+
* Create a scale bar from the top left with the given width and height.
36+
* @param x The number of pixels from the left
37+
* @param y The number of pixels from the top
38+
* @param width The width in pixels
39+
* @param height The height in pixels
40+
*/
41+
ScaleBarItem(int x, int y, int width, int height) {
42+
super(x, y, width, height)
43+
}
44+
45+
/**
46+
* Set the Map to use when calculating the map scale
47+
* @param map The Map
48+
* @return The ScaleBarItem
49+
*/
50+
ScaleBarItem map(Map map) {
51+
this.map = map
52+
this
53+
}
54+
55+
/**
56+
* Set the Units (US or METRIC) to use
57+
* @param units Units (US or METRIC)
58+
* @return The ScaleBarItem
59+
*/
60+
ScaleBarItem units(Units units) {
61+
this.units = units
62+
this
63+
}
64+
65+
/**
66+
* Set stroke Color
67+
* @param strokeColor The stroke Color
68+
* @return The ScaleBarItem
69+
*/
70+
ScaleBarItem strokeColor(Color strokeColor) {
71+
this.strokeColor = strokeColor
72+
this
73+
}
74+
75+
/**
76+
* Set the fill Color
77+
* @param fillColor The fill Color
78+
* @return The ScaleBarItem
79+
*/
80+
ScaleBarItem fillColor(Color fillColor) {
81+
this.fillColor = fillColor
82+
this
83+
}
84+
85+
/**
86+
* Set the stroke width
87+
* @param strokeWidth The stroke width
88+
* @return The ScaleBarItem
89+
*/
90+
ScaleBarItem strokeWidth(float strokeWidth) {
91+
this.strokeWidth = strokeWidth
92+
this
93+
}
94+
95+
/**
96+
* Set the Font
97+
* @param font The Font
98+
* @return The ScaleBarItem
99+
*/
100+
ScaleBarItem font(Font font) {
101+
this.font = font
102+
this
103+
}
104+
105+
/**
106+
* Set the border padding
107+
* @param border The border padding
108+
* @return The ScaleBarItem
109+
*/
110+
ScaleBarItem border(int border) {
111+
this.border = border
112+
this
113+
}
114+
115+
/**
116+
* Calculate the scale bar information
117+
* @return A ScaleBarInfo instance
118+
*/
119+
ScaleBarInfo calculateScaleBarInfo() {
120+
121+
double widthInMeters = RendererUtilities.toMeters(map.bounds.width, map.bounds.proj.crs)
122+
double widthInUnits = widthInMeters
123+
if (units == ScaleBarItem.Units.US) {
124+
widthInUnits = org.geotools.measure.Units.METRE.getConverterTo(org.geotools.measure.Units.FOOT).convert(widthInMeters)
125+
}
126+
127+
double scaleDenominator = map.scaleDenominator
128+
double pixelsPerMeter = RendererUtilities.calculatePixelsPerMeterRatio(scaleDenominator, [:])
129+
String unitForScaleText
130+
double pixelsPerUnit
131+
if (units == ScaleBarItem.Units.US) {
132+
if (widthInUnits > 5280) {
133+
unitForScaleText = "miles"
134+
pixelsPerUnit = pixelsPerMeter / 0.000621371
135+
} else {
136+
unitForScaleText = "feet"
137+
pixelsPerUnit = pixelsPerMeter / 3.28084
138+
}
139+
} else {
140+
if (widthInUnits > 1000) {
141+
unitForScaleText = "km"
142+
pixelsPerUnit = pixelsPerMeter * 1000
143+
} else {
144+
unitForScaleText = "m"
145+
pixelsPerUnit = pixelsPerMeter
146+
}
147+
}
148+
149+
int scaleBarWidthInPixels = width - border * 2
150+
double scaleBarWidthInUnits = scaleBarWidthInPixels / pixelsPerUnit
151+
152+
double orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(scaleBarWidthInUnits)))
153+
double desiredScaleBarWidthInUnits = Math.round(scaleBarWidthInUnits / orderOfMagnitude) * orderOfMagnitude
154+
double desiredScaleBarWidthInPixels = Math.round(desiredScaleBarWidthInUnits * pixelsPerUnit)
155+
156+
new ScaleBarInfo(
157+
widthInPixels: desiredScaleBarWidthInPixels,
158+
widthInUnits: desiredScaleBarWidthInUnits,
159+
unitForScaleText: unitForScaleText
160+
)
161+
}
162+
163+
static class ScaleBarInfo {
164+
165+
double widthInPixels
166+
167+
double widthInUnits
168+
169+
String unitForScaleText
170+
171+
}
172+
173+
}

src/main/groovy/geoscript/carto/SvgCartoBuilder.groovy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ class SvgCartoBuilder implements CartoBuilder {
8888
this
8989
}
9090

91+
@Override
92+
CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {
93+
java2DCartoBuilder.scaleBar(scaleBarItem)
94+
this
95+
}
96+
9197
@Override
9298
CartoBuilder grid(GridItem gridItem) {
9399
java2DCartoBuilder.grid(gridItem)

src/test/groovy/geoscript/carto/ImageCartoBuilderTest.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ class ImageCartoBuilderTest {
3737
.rectangle(new RectangleItem(20, 20, 752, 80))
3838
.text(new TextItem(30, 50, 200, 20).text("Map Title").font(new Font("Arial", Font.BOLD, 36)))
3939
.dateText(new DateTextItem(30, 85, 200, 10).font(new Font("Arial", Font.ITALIC, 18)))
40-
.scaleText(new ScaleTextItem(150, 85, 200, 10).map(map).font(new Font("Arial", Font.ITALIC, 18)))
4140
.paragraph(new ParagraphItem(250, 30, 380, 70).text("""Permission is hereby granted, free of charge, to any person obtaining a copy
4241
of this software and associated documentation files (the "Software"), to deal
4342
in the Software without restriction, including without limitation the rights
@@ -61,6 +60,8 @@ all copies or substantial portions of the Software.
6160
.row([[ID: 3, Name: "Three"]])
6261
)
6362
.legend(new LegendItem(640, 500, 120,80).addMap(map))
63+
.scaleText(new ScaleTextItem(150, 85, 200, 10).map(map).font(new Font("Arial", Font.ITALIC, 18)))
64+
.scaleBar(new ScaleBarItem(50, 125, 200, 20).map(map))
6465
.build(outputStream)
6566
}
6667
}

src/test/groovy/geoscript/carto/Java2DCartoBuilderTest.groovy

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,31 @@ class Java2DCartoBuilderTest {
2828
@Rule
2929
public TemporaryFolder temporaryFolder = new TemporaryFolder()
3030

31-
boolean showInTarget = true
31+
boolean showInTarget = false
32+
33+
@Test
34+
void drawScaleBarMiles() {
35+
File fileForShapefile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
36+
Shapefile shapefile = new Shapefile(fileForShapefile)
37+
Map map = new Map(layers: [shapefile])
38+
draw(new PageSize(400, 300), "scalebar_miles.png", { PageSize pageSize, Java2DCartoBuilder builder ->
39+
builder
40+
.map(new MapItem(0,0,400,300).map(map))
41+
.scaleBar(new ScaleBarItem(10,10,200,20).map(map).units(ScaleBarItem.Units.US))
42+
})
43+
}
44+
45+
@Test
46+
void drawScaleBarMetric() {
47+
File fileForShapefile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
48+
Shapefile shapefile = new Shapefile(fileForShapefile)
49+
Map map = new Map(layers: [shapefile])
50+
draw(new PageSize(400, 300), "scalebar_metric.png", { PageSize pageSize, Java2DCartoBuilder builder ->
51+
builder
52+
.map(new MapItem(0,0,400,300).map(map))
53+
.scaleBar(new ScaleBarItem(10,10,200,20).map(map).units(ScaleBarItem.Units.METRIC))
54+
})
55+
}
3256

3357
@Test
3458
void drawNorthArrow() {

src/test/groovy/geoscript/carto/PdfCartoBuilderTest.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ class PdfCartoBuilderTest {
3535
.rectangle(new RectangleItem(20, 20, 752, 80))
3636
.text(new TextItem(30, 50, 200, 20).text("Map Title").font(new Font("Arial", Font.BOLD, 36)))
3737
.dateText(new DateTextItem(30, 85, 200, 10).font(new Font("Arial", Font.ITALIC, 18)))
38-
.scaleText(new ScaleTextItem(150, 85, 200, 10).map(map).font(new Font("Arial", Font.ITALIC, 18)))
3938
.paragraph(new ParagraphItem(250, 30, 380, 70).text("""Permission is hereby granted, free of charge, to any person obtaining a copy
4039
of this software and associated documentation files (the "Software"), to deal
4140
in the Software without restriction, including without limitation the rights
@@ -59,6 +58,8 @@ all copies or substantial portions of the Software.
5958
.row([[ID: 3, Name: "Three"]])
6059
)
6160
.legend(new LegendItem(640, 500, 120,80).addMap(map))
61+
.scaleText(new ScaleTextItem(150, 85, 200, 10).map(map).font(new Font("Arial", Font.ITALIC, 18)))
62+
.scaleBar(new ScaleBarItem(50, 125, 200, 20).map(map))
6263
.build(outputStream)
6364
}
6465
}

0 commit comments

Comments
 (0)