Skip to content

Commit 78b5e82

Browse files
committed
Add metatile support to generating image tiles
1 parent f536d2c commit 78b5e82

3 files changed

Lines changed: 129 additions & 10 deletions

File tree

src/main/groovy/geoscript/layer/ImageTileRenderer.groovy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@ class ImageTileRenderer implements TileRenderer {
3535
}
3636

3737
@Override
38-
byte[] render(Bounds b) {
38+
byte[] render(Map options = [:], Bounds b) {
3939
map.bounds = b
40+
if (options.size) {
41+
map.width = options.size.width
42+
map.height = options.size.height
43+
}
4044
def out = new ByteArrayOutputStream()
4145
map.render(out)
4246
out.close()

src/main/groovy/geoscript/layer/TileGenerator.groovy

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
package geoscript.layer
22

33
import geoscript.geom.Bounds
4+
import geoscript.style.Fill
5+
import geoscript.style.Stroke
6+
import geoscript.workspace.Directory
7+
import geoscript.workspace.Workspace
8+
9+
import javax.imageio.ImageIO
10+
import java.awt.image.BufferedImage
411

512
/**
613
* A TileGenerator
@@ -27,19 +34,95 @@ class TileGenerator {
2734
*/
2835
void generate(Map options = [:], TileLayer tileLayer, TileRenderer renderer, int startZoom, int endZoom) {
2936
boolean missingOnly = options.get("missingOnly", false)
37+
Map metatile = options.get("metatile", [:])
38+
boolean doMetatiling = metatile && renderer instanceof ImageTileRenderer
3039
(startZoom..endZoom).each {zoom ->
3140
if (verbose) println "Zoom Level ${zoom}"
3241
long startTime = System.nanoTime()
3342
TileCursor tileCursor = options.bounds ? tileLayer.tiles(options.bounds, zoom) : tileLayer.tiles(zoom)
34-
tileCursor.eachWithIndex { Tile t, int i ->
35-
if (verbose) println " ${i}). ${t}"
36-
Bounds b = tileLayer.pyramid.bounds(t)
37-
if (verbose) println " Bounds${b}"
38-
if (!missingOnly || (missingOnly && !t.data)) {
39-
t.data = renderer.render(b)
40-
tileLayer.put(t)
41-
} else {
42-
if (verbose) println " Already generated!"
43+
if (!doMetatiling) {
44+
tileCursor.eachWithIndex { Tile t, int i ->
45+
if (verbose) println " ${i}). ${t}"
46+
Bounds b = tileLayer.pyramid.bounds(t)
47+
if (verbose) println " Bounds${b}"
48+
if (!missingOnly || (missingOnly && !t.data)) {
49+
t.data = renderer.render(b)
50+
tileLayer.put(t)
51+
} else {
52+
if (verbose) println " Already generated!"
53+
}
54+
}
55+
} else {
56+
57+
ImageTileRenderer imageTileRenderer = renderer as ImageTileRenderer
58+
59+
int metaTileWidth = metatile.width
60+
int metaTileHeight = metatile.height
61+
String imageType = metatile.imageType ?: "png"
62+
63+
int metaTileColumns = (int) (Math.ceil(((float) tileCursor.width) / ((float) metaTileWidth)))
64+
int metaTileRows = (int) (Math.ceil(((float) tileCursor.height) / ((float) metaTileHeight)))
65+
66+
int tileWidth = tileLayer.pyramid.tileWidth
67+
int tileHeight = tileLayer.pyramid.tileHeight
68+
69+
Pyramid.Origin origin = tileLayer.pyramid.origin
70+
71+
int tileCounter = 0
72+
73+
(0..<metaTileColumns).each { int c ->
74+
75+
int startX = Math.min(tileCursor.maxX, tileCursor.minX + (c * metaTileWidth))
76+
int endX = Math.min(tileCursor.maxX, startX + metaTileWidth)
77+
78+
(0..<metaTileRows).each { int r ->
79+
80+
int startY = Math.min(tileCursor.maxY, tileCursor.minY + (r * metaTileHeight))
81+
int endY = Math.min(tileCursor.maxY, startY + metaTileHeight)
82+
83+
TileCursor metaTileCursor = new TileCursor(tileLayer, zoom, startX, startY, endX, endY)
84+
Bounds bounds = metaTileCursor.bounds
85+
if (verbose) println " Metatile ${c} ${r} = ${startX},${startY} - ${endX},${endY} @ ${bounds}"
86+
87+
byte[] imageData = imageTileRenderer.render(size: [
88+
width: tileLayer.pyramid.tileWidth * metaTileCursor.width,
89+
height: tileLayer.pyramid.tileHeight * metaTileCursor.height],
90+
bounds)
91+
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData))
92+
int imageWidth = image.width
93+
int imageHeight = image.height
94+
95+
(0..<(imageWidth / tileLayer.pyramid.tileWidth)).each { int imgX ->
96+
(0..<(imageHeight / tileLayer.pyramid.tileHeight)).each { int imgY ->
97+
int imgMinX = (tileWidth * imgX)
98+
// Right
99+
if (origin == Pyramid.Origin.BOTTOM_RIGHT || origin == Pyramid.Origin.TOP_RIGHT) {
100+
imgMinX = (imageWidth - imgMinX) - tileWidth
101+
}
102+
int imgMinY = (tileHeight * imgY)
103+
// Bottom
104+
if (origin == Pyramid.Origin.BOTTOM_LEFT || origin == Pyramid.Origin.BOTTOM_RIGHT) {
105+
imgMinY = (imageHeight - imgMinY) - tileHeight
106+
}
107+
108+
long tileX = metaTileCursor.minX + imgX
109+
long tileY = metaTileCursor.minY + imgY
110+
ImageTile t = new ImageTile(zoom, tileX, tileY)
111+
if (verbose) println " ${tileCounter}). ${t}"
112+
113+
if (!missingOnly || (missingOnly && !t.data)) {
114+
BufferedImage subImage = image.getSubimage(imgMinX, imgMinY, tileWidth - 1, tileHeight - 1)
115+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()
116+
ImageIO.write(subImage, imageType, byteArrayOutputStream)
117+
t.data = byteArrayOutputStream.toByteArray()
118+
tileLayer.put(new ImageTile(zoom, tileX, tileY, byteArrayOutputStream.toByteArray()))
119+
} else {
120+
if (verbose) println " Already generated!"
121+
}
122+
tileCounter++
123+
}
124+
}
125+
}
43126
}
44127
}
45128
if (verbose) {
@@ -50,4 +133,22 @@ class TileGenerator {
50133
}
51134
}
52135

136+
static void main(String[] args) {
137+
138+
Workspace workspace = new Directory("examples/naturalearth")
139+
Layer countries = workspace.get("ne_110m_admin_0_countries")
140+
countries.style = new Fill("white") + new Stroke("black")
141+
Layer ocean = workspace.get("ne_110m_ocean")
142+
ocean.style = new Fill("blue")
143+
144+
File dir = new File("target/tiles")
145+
Pyramid pyramid = Pyramid.createGlobalMercatorPyramid(origin: Pyramid.Origin.BOTTOM_LEFT)
146+
147+
TMS tms = new TMS("world", "png", dir, pyramid)
148+
TileRenderer renderer = new ImageTileRenderer(tms, [ocean, countries])
149+
TileGenerator generator = new TileGenerator(verbose: true)
150+
generator.generate(tms, renderer, 0, 4, metatile: [width:4, height: 4])
151+
152+
}
153+
53154
}

src/test/groovy/geoscript/layer/TileGeneratorTestCase.groovy

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ class TileGeneratorTestCase {
2323
@Rule
2424
public TemporaryFolder folder = new TemporaryFolder()
2525

26+
@Test
27+
void generateTmsMetatiles() {
28+
Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
29+
shp.style = new Fill("wheat") + new Stroke("navy", 0.1)
30+
File dir = folder.newFolder("tiles")
31+
TMS tms = new TMS("states", "png", dir, Pyramid.createGlobalMercatorPyramid())
32+
ImageTileRenderer renderer = new ImageTileRenderer(tms, shp)
33+
TileGenerator generator = new TileGenerator(verbose: false)
34+
generator.generate(tms, renderer, 0, 2, metatile: [width:3, height: 3])
35+
assertNotNull tms.get(0, 0, 0).data
36+
assertNotNull tms.get(1, 1, 1).data
37+
assertNotNull tms.get(2, 2, 2).data
38+
}
39+
2640
@Test
2741
void generateMbTiles() {
2842
Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))

0 commit comments

Comments
 (0)