<|start_filename|>src/main/groovy/geoscript/layer/Tile.groovy<|end_filename|>
package geoscript.layer

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import org.apache.commons.codec.binary.Base64

/**
 * A Tile
 * @author <NAME>
 */
@EqualsAndHashCode
@ToString(includePackage = false, includeNames = true, excludes = ["data","base64String"])
class Tile {

    /**
     * The column
     */
    long x

    /**
     * The row
     */
    long y

    /**
     * The zoom level
     */
    long z

    /**
     * The data
     */
    byte[] data

    /**
     * Create a new Tile with no data
     * @param z The zoom level
     * @param x The x or column
     * @param y The y or row
     */
    Tile(long z, long x, long y) {
        this.z = z
        this.x = x
        this.y = y
    }

    /**
     * Create a new Tile with data
     * @param z The zoom level
     * @param x The x or column
     * @param y The y or row
     * @param data The array of bytes
     */
    Tile(long z, long x, long y, byte[] data) {
        this.z = z
        this.x = x
        this.y = y
        this.data = data
    }

    /**
     * Get the data as a Base64 encoded string
     * @return A Base64 encoded string
     */
    String getBase64String() {
        if (data == null) {
            null
        } else {
            new String(Base64.encodeBase64(data), "UTF-8")
        }
    }
}


<|start_filename|>src/main/groovy/geoscript/style/io/SLDWriter.groovy<|end_filename|>
package geoscript.style.io

import geoscript.style.Style
import org.geotools.factory.CommonFactoryFinder
import org.geotools.styling.NamedLayer
import org.geotools.xml.styling.SLDTransformer
import org.geotools.styling.StyleFactory
import org.geotools.styling.StyledLayerDescriptor
import org.geotools.styling.UserLayer

/**
 * Write a Style to an SLD document
 * <p><blockquote><pre>
 * def sym = new Fill("#ff00FF") + new Stroke("#ffff00", 0.25")
 * def writer = new SLDWriter()
 * writer.write(sym)
 * </pre></blockquote></p>
 * @author <NAME>
 */
class SLDWriter implements Writer {

    /**
     * Whether to format the SLD or not
     */
    boolean format = true
   
    /**
     * Write the Style to the OutputStream
     * @param options Optional named parameters
     * <ol>
     *     <li>type = Whether to export UserLayer (default) or NamedLayer </li>
     *     <li>exportDefaultValues = Whether to export default values or not (defaults to false) </li>
     *     <li>indentation = The number of spaces to use when indenting (defaults to 2) </li>
     * </ol>
     * @param style The Style
     * @param out The OutputStream
     */
    void write(Map options = [:], Style style, OutputStream out) {
        StyleFactory sf = CommonFactoryFinder.getStyleFactory(null)
        StyledLayerDescriptor sld = sf.createStyledLayerDescriptor()
        if (options.get("type", "UserLayer").equalsIgnoreCase("UserLayer")) {
            UserLayer userLayer = sf.createUserLayer()
            userLayer.addUserStyle(style.gtStyle)
            sld.addStyledLayer(userLayer)
        } else {
            NamedLayer namedLayer = sf.createNamedLayer()
            namedLayer.addStyle(style.gtStyle)
            sld.addStyledLayer(namedLayer)
        }
        def transformer = new SLDTransformer()
        transformer.exportDefaultValues = options.get("exportDefaultValues", false)
        if (format) {
            transformer.indentation = options.get("indentation", 2)
        }
        transformer.transform(sld, out)
    }

    /**
     * Write the Style to the File
     * @param options Optional named parameters
     * <ol>
     *     <li>exportDefaultValues = Whether to export default values or not (defaults to false) </li>
     *     <li>indentation = The number of spaces to use when indenting (defaults to 2) </li>
     * </ol>
     * @param style The Style
     * @param file The File
     */
    void write(Map options = [:], Style style, File file) {
        FileOutputStream out = new FileOutputStream(file)
        write(options, style, out)
        out.close()
    }

    /**
     * Write the Style to a String
     * @param options Optional named parameters
     * <ol>
     *     <li>exportDefaultValues = Whether to export default values or not (defaults to false) </li>
     *     <li>indentation = The number of spaces to use when indenting (defaults to 2) </li>
     * </ol>
     * @param The Style
     * @return A String
     */
    String write(Map options = [:], Style style) {
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        write(options, style, out);
        out.close()
        return out.toString()
    }
}

<|start_filename|>src/test/groovy/geoscript/carto/TableItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class TableItemTest {

    @Test
    void create() {

        TableItem item = new TableItem(10,20,300,400)
            .columns(["ID"]).column("Name")
            .row([[ID: 1, Name: "One"]])
            .row([[ID: 2, Name: "Two"]])
            .row([[ID: 3, Name: "Three"]])

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(2, item.columns.size())
        assertEquals(3, item.rows.size())
        assertTrue(item.toString().startsWith("TableItem(x = 10, y = 20, width = 300, height = 400, columns = [ID, Name], rows = [[ID:1, Name:One], [ID:2, Name:Two], [ID:3, Name:Three]], "))
    }


}


<|start_filename|>src/main/groovy/geoscript/layer/TileGenerator.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import javax.imageio.ImageIO
import java.awt.image.BufferedImage

/**
 * A TileGenerator
 * @author <NAME>
 */
class TileGenerator {

    /**
     * Whether to verbosely print status or not
     */
    boolean verbose = false

    /**
     * Generate Tiles for a TileLayer using a TileRenderer
     * @param options The optional named parameters:
     * <ul>
     *     <li>bounds = The Bounds of the Tiles to generate</li>
     *     <li>missingOnly = Whether to only generate missing tiles (true) or all tiles (false)</li>
     * </ul>
     * @param tileLayer The TileLayer
     * @param renderer The TileRenderer
     * @param startZoom The start zoom level
     * @param endZoom The end zoom level
     */
    void generate(Map options = [:], TileLayer tileLayer, TileRenderer renderer, int startZoom, int endZoom) {
        boolean missingOnly = options.get("missingOnly", false)
        Map metatile = options.get("metatile", [:])
        boolean doMetatiling = metatile && renderer instanceof ImageTileRenderer
        (startZoom..endZoom).each {zoom ->
            if (verbose) println "Zoom Level ${zoom}"
            long startTime = System.nanoTime()
            TileCursor tileCursor = options.bounds ? tileLayer.tiles(options.bounds, zoom) : tileLayer.tiles(zoom)
            if (!doMetatiling) {
                tileCursor.eachWithIndex { Tile t, int i ->
                    if (verbose) println "   ${i}). ${t}"
                    Bounds b = tileLayer.pyramid.bounds(t)
                    if (verbose) println "          Bounds${b}"
                    if (!missingOnly || (missingOnly && !t.data)) {
                        t.data = renderer.render(b)
                        tileLayer.put(t)
                    } else {
                        if (verbose) println "          Already generated!"
                    }
                }
            } else {

                ImageTileRenderer imageTileRenderer = renderer as ImageTileRenderer

                int metaTileWidth = metatile.width
                int metaTileHeight = metatile.height
                String imageType = metatile.imageType ?: "png"

                int metaTileColumns = (int) (Math.ceil(((float) tileCursor.width) / ((float) metaTileWidth)))
                int metaTileRows = (int) (Math.ceil(((float) tileCursor.height) / ((float) metaTileHeight)))

                int tileWidth = tileLayer.pyramid.tileWidth
                int tileHeight = tileLayer.pyramid.tileHeight

                Pyramid.Origin origin = tileLayer.pyramid.origin

                int tileCounter = 0

                (0..<metaTileColumns).each { int c ->

                    int startX = Math.min(tileCursor.maxX, tileCursor.minX + (c * metaTileWidth))
                    int endX =  Math.min(tileCursor.maxX, startX + metaTileWidth)

                    (0..<metaTileRows).each { int r ->

                        int startY = Math.min(tileCursor.maxY, tileCursor.minY + (r * metaTileHeight))
                        int endY =  Math.min(tileCursor.maxY, startY + metaTileHeight)

                        TileCursor metaTileCursor = new TileCursor(tileLayer, zoom, startX, startY, endX, endY)
                        Bounds bounds = metaTileCursor.bounds
                        if (verbose) println "   Metatile ${c} ${r} = ${startX},${startY} - ${endX},${endY} @ ${bounds}"

                        byte[] imageData = imageTileRenderer.render(size: [
                            width: tileLayer.pyramid.tileWidth * metaTileCursor.width,
                            height: tileLayer.pyramid.tileHeight * metaTileCursor.height],
                            bounds
                        )
                        BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData))
                        int imageWidth = image.width
                        int imageHeight = image.height

                        (0..<(imageWidth / tileLayer.pyramid.tileWidth)).each { int imgX ->
                            (0..<(imageHeight / tileLayer.pyramid.tileHeight)).each { int imgY ->
                                int imgMinX = (tileWidth * imgX)
                                // Right
                                if (origin == Pyramid.Origin.BOTTOM_RIGHT || origin == Pyramid.Origin.TOP_RIGHT) {
                                    imgMinX = (imageWidth - imgMinX) - tileWidth
                                }
                                int imgMinY = (tileHeight * imgY)
                                // Bottom
                                if (origin == Pyramid.Origin.BOTTOM_LEFT || origin == Pyramid.Origin.BOTTOM_RIGHT) {
                                    imgMinY = (imageHeight - imgMinY) - tileHeight
                                }

                                long tileX = metaTileCursor.minX + imgX
                                long tileY = metaTileCursor.minY + imgY
                                ImageTile t = new ImageTile(zoom, tileX, tileY)
                                if (verbose) println "      ${tileCounter}). ${t}"

                                if (!missingOnly || (missingOnly && !t.data)) {
                                    BufferedImage subImage = image.getSubimage(imgMinX, imgMinY, tileWidth - 1, tileHeight - 1)
                                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()
                                    ImageIO.write(subImage, imageType, byteArrayOutputStream)
                                    t.data = byteArrayOutputStream.toByteArray()
                                    tileLayer.put(new ImageTile(zoom, tileX, tileY, byteArrayOutputStream.toByteArray()))
                                } else {
                                    if (verbose) println "          Already generated!"
                                }
                                tileCounter++
                            }
                        }
                    }
                }
            }
            if (verbose) {
                double endTime = System.nanoTime() - startTime
                int numberOfTiles = options.bounds ? tileLayer.tiles(options.bounds, zoom).size : tileLayer.pyramid.grid(zoom).size
                println "   Generating ${numberOfTiles} tile${numberOfTiles > 1 ? 's':''} took ${endTime / 1000000000.0} seconds"
            }
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/Java2DCartoBuilderTest.groovy<|end_filename|>
package geoscript.carto

import geoscript.filter.Color
import geoscript.layer.GeoTIFF
import geoscript.layer.Raster
import geoscript.layer.Shapefile
import geoscript.render.Map
import geoscript.style.ColorMap
import geoscript.style.Fill
import geoscript.style.Shape
import geoscript.style.Stroke
import geoscript.style.UniqueValues
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import java.awt.Font

import static org.junit.jupiter.api.Assertions.*

import javax.imageio.ImageIO
import java.awt.Graphics2D
import java.awt.RenderingHints
import java.awt.image.BufferedImage;

class Java2DCartoBuilderTest {

    @TempDir
    private File folder

    boolean showInTarget = false

    @Test
    void drawScaleBarMiles() {
        File fileForShapefile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(fileForShapefile)
        Map map = new Map(layers: [shapefile])
        draw(new PageSize(400, 300), "scalebar_miles.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder
                .map(new MapItem(0,0,400,300).map(map))
                .scaleBar(new ScaleBarItem(10,10,200,20).map(map).units(ScaleBarItem.Units.US))
        })
    }

    @Test
    void drawScaleBarMetric() {
        File fileForShapefile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(fileForShapefile)
        Map map = new Map(layers: [shapefile])
        draw(new PageSize(400, 300), "scalebar_metric.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder
                .map(new MapItem(0,0,400,300).map(map))
                .scaleBar(new ScaleBarItem(10,10,200,20).map(map).units(ScaleBarItem.Units.METRIC))
        })
    }

    @Test
    void drawNorthArrow() {
        draw(new PageSize(80, 140), "northarrow.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.northArrow(new NorthArrowItem(0,0,pageSize.width, pageSize.height))
        })
    }

    @Test
    void drawNorthArrowWithText() {
        draw(new PageSize(80, 140), "northarrow_text.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.northArrow(new NorthArrowItem(0,0,pageSize.width, pageSize.height).drawText(true))
        })
    }

    @Test
    void drawNESWArrow() {
        draw(new PageSize(200, 200), "northarrow_nesw.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.northArrow(new NorthArrowItem(0, 0, pageSize.width, pageSize.height).style(NorthArrowStyle.NorthEastSouthWest))
        })
    }

    @Test
    void drawNESWArrowWithText() {
        draw(new PageSize(200, 200), "northarrow_nesw_text.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.northArrow(new NorthArrowItem(0, 0, pageSize.width, pageSize.height)
                .style(NorthArrowStyle.NorthEastSouthWest)
                .drawText(true)
                .font(new Font("Arial", Font.BOLD, 24))
            )
        })
    }

    @Test
    void drawLegend() {
        draw(new PageSize(150, 160), "legend.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.legend(new LegendItem(0, 0, pageSize.width, pageSize.height)
                    .title("Legend")
                    .addPointEntry("Cities", new Shape("Black", 8, "circle"))
                    .addLineEntry("Rivers", new Stroke("Blue", 2))
                    .addPolygonEntry("Parcels", new Fill("red", 0.75) + new Stroke("black",1))
            )
        })
    }

    @Test
    void drawLegendForLayer() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(file)
        shapefile.style = new Fill("white") + new Stroke("black", 0.1)
        draw(new PageSize(150, 150), "legend_layer_simple.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.legend(new LegendItem(0, 0, pageSize.width, pageSize.height)
                    .title("Legend")
                    .addLayer(shapefile)
            )
        })
    }

    @Test
    void drawLegendForLayerWithCategories() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(file)
        shapefile.style = new UniqueValues(shapefile, shapefile.schema.field("SUB_REGION"))
        draw(new PageSize(300, 800), "legend_layer_categories.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.legend(new LegendItem(0, 0, pageSize.width, pageSize.height)
                    .title("Legend")
                    .addLayer(shapefile)
            )
        })
    }

    @Test
    void drawLegendForLayerAndRaster() {
        File fileForShapefile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(fileForShapefile)
        File fileForRaster = new File(getClass().getClassLoader().getResource("raster.tif").toURI())
        Raster raster = new GeoTIFF(fileForRaster).read()
        draw(new PageSize(300, 800), "legend_layer_raster.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.legend(new LegendItem(0, 0, pageSize.width, pageSize.height)
                    .title("Legend")
                    .addLayer(shapefile)
                    .addRaster(raster)
            )
        })
    }

    @Test
    void drawLegendForMap() {
        File fileForShapefile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(fileForShapefile)
        File fileForRaster = new File(getClass().getClassLoader().getResource("raster.tif").toURI())
        Raster raster = new GeoTIFF(fileForRaster).read()
        Map map = new Map(layers: [shapefile, raster])

        draw(new PageSize(300, 800), "legend_map.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.legend(new LegendItem(0, 0, pageSize.width, pageSize.height)
                .title("Legend")
                .addMap(map)
            )
        })
    }

    @Test
    void drawLegendForRasterWithoutColorMap() {
        File file = new File(getClass().getClassLoader().getResource("raster.tif").toURI())
        Raster raster = new GeoTIFF(file).read()
        Raster smallRaster = raster.resample(size: [50, 30])
        draw(new PageSize(300, 300), "legend_image.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.legend(new LegendItem(0, 0, pageSize.width, pageSize.height)
                .title("Legend")
                .addGroupEntry("Vector")
                .addPointEntry("Cities", new Shape("Black", 8, "circle"))
                .addLineEntry("Rivers", new Stroke("Blue", 2))
                .addPolygonEntry("Parcels", new Fill("red", 0.75) + new Stroke("black",1))
                .addGroupEntry("Raster")
                .addImageEntry("Elevation", smallRaster.bufferedImage)
            )
        })
    }

    @Test
    void drawLegendWithMultipleColumns() {

        List units = [
            ["AHa",175,0,111],
            ["AHat",192,54,22],
            ["AHcf",150,70,72],
            ["AHh",109,13,60],
            ["AHpe",232,226,82],
            ["AHt",99,0,95],
            ["AHt3",233,94,94],
            ["Aa1",255,236,207],
            ["Aa2",145,73,76],
            ["Aa3",254,212,164],
            ["Aa4",212,109,19],
            ["Aa5",175,66,28],
            ["Aam",92,87,129],
            ["Aau",129,0,0],
            ["Ach",22,0,219],
            ["Achu",79,82,246],
            ["Achp",55,177,183],
            ["Ad",186,60,0],
            ["Adc",254,190,0],
            ["Adl",247,146,29],
            ["Ae",0,198,40],
            ["Ael1",237,64,45],
            ["Ael2",255,147,135],
            ["Ael3",209,183,153],
            ["Ael4",123,135,209],
            ["Ah4",210,172,60],
            ["Ah5",169,212,254],
            ["Ah6",109,166,16],
            ["Ah7",104,142,37],
            ["Ah8",164,195,12],
            ["Am",19,163,171],
            ["Aml",0,99,66],
            ["Amm",102,192,9],
            ["Amu",121,219,224],
            ["Aoa1",239,64,46],
            ["Aoa2",254,194,194],
            ["Aoa3",168,20,20],
            ["Aoa4",201,0,219],
            ["Aop",254,172,0],
            ["Aos",234,97,50],
            ["Api",255,255,255]
        ]

        draw(new PageSize(700, 300), "legend_cols.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            LegendItem legendItem = new LegendItem(0, 0, pageSize.width, pageSize.height).title("Mars Geology")
            units.each { List values ->
                String color = "${values[1]},${values[2]},${values[3]}"
                legendItem.addPolygonEntry(values[0], new Fill(color) + new Stroke("black",1))
            }
            builder.legend(legendItem)
        })
    }

    @Test
    void drawLegendWithRasterColorMap() {
        draw(new PageSize(240, 350), "legend_rastercolormap.png", { PageSize pageSize, Java2DCartoBuilder builder ->
            builder.legend(new LegendItem(0, 0, pageSize.width, pageSize.height)
                    .title("Legend")
                    .addPointEntry("Cities", new Shape("Black", 8, "circle"))
                    .addLineEntry("Rivers", new Stroke("Blue", 2))
                    .addPolygonEntry("Parcels", new Fill("red", 0.75) + new Stroke("black",1))
                    .addColorMapEntry("Elevation", new ColorMap(0, 2300, Color.getPaletteColors("MutedTerrain", 5)))
                    .addColorMapEntry("Potential", new ColorMap(0, 2300, Color.getPaletteColors("YellowToRedHeatMap", 7)))
                    .addPointEntry("Sites", new Shape("Red", 10, "star"))
            )
        })
    }


    private void draw(PageSize pageSize, String fileName, Closure closure) {
        BufferedImage image = new BufferedImage(pageSize.width, pageSize.height, BufferedImage.TYPE_INT_ARGB)
        Graphics2D graphics = image.createGraphics()
        graphics.renderingHints = [
                (RenderingHints.KEY_ANTIALIASING)     : RenderingHints.VALUE_ANTIALIAS_ON,
                (RenderingHints.KEY_TEXT_ANTIALIASING): RenderingHints.VALUE_TEXT_ANTIALIAS_ON
        ]
        Java2DCartoBuilder builder = new Java2DCartoBuilder(graphics, pageSize)
        closure.call(pageSize, builder)
        File file = getTempFile(fileName)
        ImageIO.write(image, "png", file)
        assertTrue(file.exists())
        assertTrue(file.length() > 1)
    }

    private File getTempFile(String fileName) {
        new File(getDirectory(), fileName)
    }

    private File getDirectory() {
        if (showInTarget) {
            new File("target")
        } else {
            File dir = new File(folder, "carto")
            dir.mkdir()
            dir
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/process/ProcessTest.groovy<|end_filename|>
package geoscript.process

import geoscript.feature.Field
import geoscript.workspace.Memory
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.*
import geoscript.layer.*
import org.geotools.process.Processors
import org.opengis.feature.type.Name
import org.geotools.feature.NameImpl
import org.geotools.process.ProcessFactory

/**
 *  The Process UnitTest
 *  @author <NAME>
 */
class ProcessTest {
    
    private Layer getStatesLayer () {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        new Shapefile(file)
    }
    
    @Test void conversions() {
        def pt = new Point(0,0)
        assertTrue Process.convert(pt, org.locationtech.jts.geom.Geometry) instanceof org.locationtech.jts.geom.Geometry
        assertTrue Process.convert(pt.g, geoscript.geom.Geometry) instanceof geoscript.geom.Geometry

        def b = new Bounds(0,0,10,10,"EPSG:4326")
        assertTrue Process.convert(b, org.geotools.geometry.jts.ReferencedEnvelope) instanceof org.geotools.geometry.jts.ReferencedEnvelope
        assertTrue Process.convert(b.env, geoscript.geom.Bounds) instanceof geoscript.geom.Bounds

        def layer = getStatesLayer()
        assertTrue Process.convert(layer, org.geotools.feature.FeatureCollection) instanceof org.geotools.feature.FeatureCollection
        assertTrue Process.convert(layer.fs.features, geoscript.layer.Layer) instanceof geoscript.layer.Layer
        assertTrue Process.convert(layer.fs.features, geoscript.layer.Cursor) instanceof geoscript.layer.Cursor
        
        assertTrue Process.convert(layer.cursor, org.geotools.feature.FeatureCollection) instanceof org.geotools.feature.FeatureCollection
        assertTrue Process.convert(layer.fs.features, geoscript.layer.Cursor) instanceof geoscript.layer.Cursor
    }
    
    @Test void getProcessNames() {
        def names = Process.processNames
        assertTrue names.size() > 0
    }
    
    @Test void boundsGeoToolsProcess() {
        def names = Process.processNames
        String processName = "gs:Bounds" in names ? "gs:Bounds" : "vec:Bounds"
        def p = new Process(processName)
        assertEquals processName, p.name
        assertEquals "Bounds", p.title
        assertEquals "Computes the bounding box of the input features.", p.description
        assertEquals "1.0.0", p.version
        assertEquals processName, p.toString()
        
        assertEquals 1, p.parameters.size()
        assertTrue p.parameters.containsKey("features")
        assertEquals geoscript.layer.Cursor, p.parameters.get("features")
        
        assertEquals 1, p.results.size()
        assertTrue p.results.containsKey("bounds")
        assertEquals geoscript.geom.Bounds, p.results.get("bounds")

        Layer layer = getStatesLayer()
        Map results = p.execute(["features": layer])
        assertTrue results.containsKey("bounds")
        assertTrue results.bounds instanceof geoscript.geom.Bounds

        results = p.execute(["features": layer.getCursor("STATE_NAME = 'Washington' OR STATE_NAME = 'Oregon'")])
        assertTrue results.containsKey("bounds")
        assertTrue results.bounds instanceof geoscript.geom.Bounds
    }
    
    @Test void countGeoToolsProcess() {
        def names = Process.processNames
        String processName = "gs:Count" in names ? "gs:Count" : "vec:Count"
        def p = new Process(processName)
        assertEquals processName, p.name
        assertEquals "Count Features", p.title
        assertEquals "Computes the number of features in a feature collection.", p.description
        assertEquals "1.0.0", p.version
        assertEquals processName, p.toString()

        assertEquals 1, p.parameters.size()
        assertTrue p.parameters.containsKey("features")
        assertEquals geoscript.layer.Cursor, p.parameters.get("features")

        assertEquals 1, p.results.size()
        assertTrue p.results.containsKey("result")
        assertEquals java.lang.Number, p.results.get("result")

        Layer layer = getStatesLayer()
        Map results = p.execute(["features": layer])
        assertTrue results.containsKey("result")
        assertEquals 49, results.result

        results = p.execute(["features": layer.getCursor("STATE_NAME = 'Washington' OR STATE_NAME = 'Oregon'")])
        assertTrue results.containsKey("result")
        assertEquals 2, results.result
    }

    @Test void collectGeometriesGeoToolsProcess() {
        def names = Process.processNames
        String processName = "gs:CollectGeometries" in names ? "gs:CollectGeometries" : "vec:CollectGeometries"
        def p = new Process(processName)
        assertEquals processName, p.name
        assertEquals "Collect Geometries", p.title
        assertEquals "Collects the default geometries of the input features and combines them into a single geometry collection", p.description
        assertEquals "1.0.0", p.version
        assertEquals processName, p.toString()

        assertEquals 1, p.parameters.size()
        assertTrue p.parameters.containsKey("features")
        assertEquals geoscript.layer.Cursor, p.parameters.get("features")

        assertEquals 1, p.results.size()
        assertTrue p.results.containsKey("result")
        assertEquals geoscript.geom.Geometry, p.results.get("result")

        Layer layer = getStatesLayer()
        Map results = p.execute(["features": layer])
        assertTrue results.containsKey("result")
        assertTrue results.result instanceof geoscript.geom.Geometry
        assertEquals 39, (results.result as Geometry).numGeometries

        results = p.execute(["features": layer.getCursor("STATE_NAME = 'Washington' OR STATE_NAME = 'Oregon'")])
        assertTrue results.containsKey("result")
        assertTrue results.result instanceof geoscript.geom.Geometry
        assertEquals 3, (results.result as Geometry).numGeometries
    }
    
    @Test void convexHullClosureProcess() {
        Process p = new Process("convexhull",
            "Create a convexhull around the features",
            [features: geoscript.layer.Cursor],
            [result: geoscript.layer.Cursor],
            { inputs ->
                def geoms = new GeometryCollection(inputs.features.collect{f -> f.geom})
                def output = new Layer()
                output.add([geoms.convexHull])
                [result: output]
            }
        )
        assertEquals "geoscript:convexhull", p.name
        assertEquals "convexhull", p.title
        assertEquals "Create a convexhull around the features", p.description
        assertEquals "1.0.0", p.version
        assertEquals "geoscript:convexhull", p.toString()

        assertEquals 1, p.parameters.size()
        assertTrue p.parameters.containsKey("features")
        assertEquals geoscript.layer.Cursor, p.parameters.get("features")

        assertEquals 1, p.results.size()
        assertTrue p.results.containsKey("result")
        assertEquals geoscript.layer.Cursor, p.results.get("result")

        Layer layer = getStatesLayer()
        Map results = p.execute([features: layer.cursor])
        assertTrue results.result instanceof geoscript.layer.Cursor
        Cursor c = results.result
        c.each {f -> f.geom instanceof Polygon}

        // Make sure the Closure Process is registered correctly by looking it up by name
        p = new Process("geoscript:convexhull")
        assertNotNull p
        assertNotNull p.process != null

        // Execute the underlying GeoTools Process (inputs and output should be GeoTools objects)
        Map convexHullResults = p.process.execute([features: layer.fs.features])
        assertTrue convexHullResults.result instanceof org.geotools.feature.FeatureCollection

        // Make sure the ProcessFactory works
        Name name = new NameImpl("geoscript","convexhull")
        ProcessFactory factory = Processors.createProcessFactory(name)
        assertEquals "geoscript:convexhull", name.toString()
        assertEquals "convexhull", factory.getTitle(name).toString()
        assertEquals "Create a convexhull around the features", factory.getDescription(name).toString()
        assertEquals "1.0.0", factory.getVersion(name)

        // The ParameterInfo should be GeoTools types
        Map paramInfo = factory.getParameterInfo(name)
        assertEquals 1, paramInfo.size()
        assertTrue paramInfo.containsKey("features")
        def param = paramInfo.get("features")
        assertTrue param instanceof org.geotools.data.Parameter
        assertTrue org.geotools.feature.FeatureCollection.isAssignableFrom(param.type)

        // The ResultInfo should be GeoTools types
        Map resultInfo = factory.getResultInfo(name,[:])
        assertEquals 1, resultInfo.size()
        assertTrue resultInfo.containsKey("result")
        param = resultInfo.get("result")
        assertTrue param instanceof org.geotools.data.Parameter
        assertTrue org.geotools.feature.FeatureCollection.isAssignableFrom(param.type)
    }

    @Test void centroidsClosureProcess() {
        Process p = new Process("centroids",
                "Create a centroid from the geometry for each feature",
                [features: geoscript.layer.Cursor],
                [result: geoscript.layer.Cursor],
                { inputs ->
                    def output = new Layer()
                    inputs.features.each{f -> output.add(["geom": f.geom.centroid])}
                    [result: output]
                }
        )
        assertEquals "geoscript:centroids", p.name
        assertEquals "centroids", p.title
        assertEquals "Create a centroid from the geometry for each feature", p.description
        assertEquals "1.0.0", p.version
        assertEquals "geoscript:centroids", p.toString()

        assertEquals 1, p.parameters.size()
        assertTrue p.parameters.containsKey("features")
        assertEquals geoscript.layer.Cursor, p.parameters.get("features")

        assertEquals 1, p.results.size()
        assertTrue p.results.containsKey("result")
        assertEquals geoscript.layer.Cursor, p.results.get("result")

        Layer layer = getStatesLayer()
        Map results = p.execute([features: layer.cursor])
        assertTrue results.result instanceof geoscript.layer.Cursor
        Cursor c = results.result
        int count = 0
        c.each {f -> f.geom instanceof Point; count++}
        assertEquals 49, count
    }

    /*@Test void rasterAddProcess() {
        Bounds bounds = new Bounds(0, 0, 7, 5, "EPSG:4326")
        List data1 = [
                [0,0,0,0,0,0,0],
                [0,1,1,1,1,1,0],
                [0,1,2,3,2,1,0],
                [0,1,1,1,1,1,0],
                [0,0,0,0,0,0,0]
        ]
        Raster raster1 = new Raster(data1, bounds)

        List data2 = [
                [1,1,1,1,1,1,1],
                [1,2,2,2,2,2,1],
                [1,2,3,4,3,2,1],
                [1,2,2,2,2,2,1],
                [1,1,1,1,1,1,1]
        ]
        Raster raster2 = new Raster(data2, bounds)

        Process p = new Process("ras:AddCoverages")
        Map results = p.execute(["coverageA": raster1, "coverageB": raster2])
        Raster raster3 = results.result

        assertEquals 1, raster3.eval(new Point(0.5,0.5))[0], 0.1
        assertEquals 3, raster3.eval(new Point(1.5,1.5))[0], 0.1
        assertEquals 5, raster3.eval(new Point(2.5,2.5))[0], 0.1
        assertEquals 7, raster3.eval(new Point(3.5,2.5))[0], 0.1
    }*/

    @Test void rasterToPointsProcess() {
        Bounds bounds = new Bounds(0, 0, 7, 5, "EPSG:4326")
        List data = [
                [0,0,0,0,0,0,0],
                [0,1,1,1,1,1,0],
                [0,1,2,3,2,1,0],
                [0,1,1,1,1,1,0],
                [0,0,0,0,0,0,0]
        ]
        Raster raster = new Raster(data, bounds)

        Process p = new Process("ras:RasterAsPointCollection")
        Map results = p.execute(["data": raster])
        Cursor c = results.result
        int count = 0
        c.each {f -> f.geom instanceof Point; count++}
        assertEquals 35, count
    }

    @Test void vectorToRasterProcess() {
        Bounds bounds = new Bounds(0,0,10,11,"EPSG:4326")
        Geometry geom = bounds.getGrid(5,5,"poygon")
        Layer layer = new Memory().create("grid",[
            new Field("geom","Polygon","EPSG:4326"),
            new Field("value","Double")
        ])
        geom.geometries.eachWithIndex{g,i ->
            layer.add([geom:g, value: i + 10])
        }
        Process p = new Process("vec:VectorToRaster")
        Map results = p.execute([
            features: layer,
            rasterWidth: 400,
            rasterHeight: 400,
            attribute: "value",
            bounds: layer.bounds
        ])
        Raster raster = results.result
        assertNotNull raster
        assertEquals layer.bounds, raster.bounds
    }
}


<|start_filename|>src/test/groovy/geoscript/render/Base64Test.groovy<|end_filename|>
package geoscript.render

import geoscript.layer.Layer
import geoscript.layer.Shapefile
import geoscript.style.Fill
import geoscript.style.Stroke
import org.junit.jupiter.api.Test

import javax.imageio.ImageIO

import static org.junit.jupiter.api.Assertions.*

/**
 * The Base64 Unit Test
 * @author <NAME>
 */
class Base64Test {

    @Test
    void renderToString() {
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Layer layer = new Shapefile(shpFile)
        layer.style = new Stroke('black', 0.1) + new Fill('gray', 0.75)
        Map map = new Map(layers: [layer], backgroundColor: "white")
        // PNG with prefix
        Base64 base64 = new Base64()
        String str = base64.render(map)
        assertNotNull(str)
        assertTrue(str.startsWith("image/png;base64,"))
        // PNG with out prefix
        base64 = new Base64(renderer: new PNG(), includePrefix: false)
        str = base64.render(map)
        assertNotNull(str)
        assertFalse(str.startsWith("image/png;base64,"))
        // JPEG with prefix
        base64 = new Base64(renderer: new JPEG(), includePrefix: false)
        str = base64.render(map)
        assertNotNull(str)
        assertFalse(str.startsWith("image/jpeg;base64,"))
        // GIF with prefix
        base64 = new Base64(renderer: new GIF(), includePrefix: false)
        str = base64.render(map)
        assertNotNull(str)
        assertFalse(str.startsWith("image/gif;base64,"))
    }

    @Test
    void renderToOutputStream() {
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Layer layer = new Shapefile(shpFile)
        layer.style = new Stroke('black', 0.1) + new Fill('gray', 0.75)
        Map map = new Map(layers: [layer], backgroundColor: "white")
        Base64 base64 = new Base64()
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        base64.render(map, out)
        byte[] bytes = out.toByteArray()
        assertTrue bytes.length > 0
    }

}


<|start_filename|>src/test/groovy/geoscript/style/DatabaseStyleRepositoryTest.groovy<|end_filename|>
package geoscript.style

import geoscript.filter.Color
import geoscript.style.io.SLDWriter
import geoscript.workspace.H2
import groovy.sql.Sql
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

class DatabaseStyleRepositoryTest {

    @TempDir
    File folder

    @Test
    void sqlite() {
        File databaseFile = new File(folder,"styles.db")
        Sql sql = Sql.newInstance("jdbc:sqlite:${databaseFile.absolutePath}", "org.sqlite.JDBC")
        runDatabaseTest(DatabaseStyleRepository.forSqlite(sql))
    }

    @Test
    void h2() {
        File databaseFile = new File(folder,"styles.db")
        H2 h2 = new H2(databaseFile)
        Sql sql = h2.sql
        runDatabaseTest(DatabaseStyleRepository.forH2(sql))
    }

    private String createStyle(Color color) {
        new SLDWriter().write(new Fill(color))
    }

    private void runDatabaseTest(DatabaseStyleRepository styleRepository) {

        File file = new File(getClass().getClassLoader().getResource("states.sld").toURI())
        assertNotNull(file)

        // States
        styleRepository.save("states", "states", file.text)
        styleRepository.save("states", "states_blue", createStyle(new Color("blue")))
        styleRepository.save("states", "states_red", createStyle(new Color("red")))
        // Cities
        styleRepository.save("cities", "cities", createStyle(new Color("gray")))
        styleRepository.save("cities", "cities_black", createStyle(new Color("black")))

        String sld = styleRepository.getDefaultForLayer("states")
        assertNotNull(sld)
        assertTrue(sld.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"))

        List<Map<String, String>> styles = styleRepository.getForLayer("states")
        assertEquals(3, styles.size())
        assertEquals("states", styles[0].layerName)
        assertEquals("states", styles[0].styleName)
        assertTrue(styles[0].style.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"))

        styles = styleRepository.getAll()
        assertEquals(5, styles.size())
        assertEquals("states", styles[0].layerName)
        assertEquals("states", styles[0].styleName)
        assertTrue(styles[0].style.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"))

        styleRepository.delete("states", "states")
        assertEquals(4, styleRepository.getAll().size())

    }

}


<|start_filename|>src/main/groovy/geoscript/layer/io/Pbf.groovy<|end_filename|>
package geoscript.layer.io

import no.ecc.vectortile.VectorTileDecoder
import no.ecc.vectortile.VectorTileEncoder
import org.geotools.geometry.jts.JTS
import org.geotools.referencing.operation.transform.ProjectiveTransform
import org.geotools.renderer.lite.RendererUtilities
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.Envelope
import org.locationtech.jts.geom.Geometry as JtsGeometry
import org.locationtech.jts.geom.GeometryFactory
import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.filter.Filter
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.layer.Layer
import geoscript.proj.Projection
import geoscript.workspace.Memory
import org.geotools.renderer.crs.ProjectionHandler
import org.geotools.renderer.crs.ProjectionHandlerFinder
import org.opengis.referencing.operation.MathTransform

import java.awt.Rectangle

/**
 * A MapBox Vector Tile Reader and Writer
 */
class Pbf {

    /**
     * Read a List of Layers
     * @param options The optional named parameters
     * <ul>
     *    <li>proj = The Projection (defaults to EPSG:3857)</li>
     *    <li>size = The tile size (defaults to 256)</li>
     * </ul>
     * @param byte The array of bytes
     * @param b The Bounds
     */
    static List<Layer> read(Map options = [:], byte[] bytes, Bounds b) {

        ProjectionHandler projectionHandler = ProjectionHandlerFinder.getHandler(b.env, b.proj.crs, true)
        Projection proj = options.get("proj", new Projection("EPSG:3857"))
        int tileSize = options.get("tileSize", 256)
        int extent = options.get("extent", 4096)

        Map<String, Layer> layers = [:]
        VectorTileDecoder vectorTileDecoder = new VectorTileDecoder()
        VectorTileDecoder.FeatureIterable  features = vectorTileDecoder.decode(bytes)
        features.iterator().each { VectorTileDecoder.Feature vectorTileFeature ->
            String layerName = vectorTileFeature.layerName
            Geometry geometry = fromPixel(Geometry.wrap(vectorTileFeature.geometry), b, tileSize, extent)
            if (!geometry.isEmpty()) {
                if (projectionHandler) {
                    JtsGeometry processedGeom = projectionHandler.preProcess(geometry.g)
                    if (processedGeom) {
                        geometry = Geometry.wrap(processedGeom)
                    }
                }
                Map<String, Object> attributes = [geometry: geometry]
                attributes.putAll(vectorTileFeature.attributes)
                if (!layers.containsKey(layerName)) {
                    Feature feature = new Feature(attributes, "1")
                    Schema schema = new Schema(layerName, feature.schema.fields).reproject(proj)
                    layers[layerName] = new Memory().create(schema)
                }
                Layer layer = layers[layerName]
                Feature feature = layer.schema.feature(attributes)
                layer.add(feature)
            }
        }

        layers.values().toList()
    }

    /**
     * Convert a Geometry from pixel coordinates to geographic coordinates
     * @param g The Geometry in pixel coordinates
     * @param b The Bounds in geographic coordinates
     * @param tileSize The tile size
     * @return A Geometry in geographic coordinates
     */
    private static Geometry fromPixel(Geometry g, Bounds b, int size, int extent) {
        // Clone the Geometry in pixel coordinates
        GeometryFactory factory = new GeometryFactory()
        Geometry gpx = Geometry.wrap(factory.createGeometry(g.g))
        // Convert a Coordinate from pixel to geographic
        gpx.coordinates.each { Coordinate c ->
            c.y = extent - c.y
            double px = c.x / extent
            double py = c.y / extent
            c.x = b.minX + (b.width * px)
            c.y = b.minY + (b.height * py)
        }
        gpx
    }

    /**
     * Write a List of Layers
     * @param options The optional named parameters
     * <ul>
     *   <li>tileSize = The tile size (defaults to 256)</li>
     * </ul>
     * @param layers The List of Layers
     * @param b The Bounds
     * @return An array of bytes
     */
    static byte[] write(Map options = [:], List<Layer> layers, Bounds b) {
        int tileSize = options.get("tileSize", 256)
        Map<String, List> subFields = options.get("subFields")

        Envelope clipEnvelope = new Envelope(b.env)
        final double bufferWidth = b.env.width * 0.1f
        final double bufferHeight = b.env.height * 0.1f
        clipEnvelope.expandBy(bufferWidth, bufferHeight)

        VectorTileEncoder encoder = new VectorTileEncoder()
        layers.each { Layer layer ->
            Bounds projectedBounds = b.reproject(layer.proj)
            projectedBounds.expandBy(projectedBounds.width * 0.1f)
            Geometry boundsGeom = projectedBounds.geometry
            boolean isPoint = layer.schema.geom.typ.equalsIgnoreCase("point")
            // Intersects
            List<JtsGeometry> geometries = []
            layer.eachFeature(Filter.intersects(layer.schema.geom.name, boundsGeom), { Feature f ->
                // Clip
                Geometry geom = isPoint ? f.geom : f.geom.intersection(boundsGeom)
                if (!geom.empty) {
                    // Process
                    ProjectionHandler projectionHandler = ProjectionHandlerFinder.getHandler(b.env, layer.proj.crs, true)
                    if (projectionHandler) {
                        JtsGeometry processedGeom = projectionHandler.preProcess(geom.g)
                        if (processedGeom) {
                            geom = Geometry.wrap(processedGeom)
                        }
                    }
                    // Reproject
                    JtsGeometry geometry = Projection.transform(geom, layer.proj, b.proj).g
                    Map attributes = [:]
                    layer.schema.fields.each { Field fld ->
                        if (!fld.isGeometry()) {
                            if (subFields == null
                                    || subFields.isEmpty()
                                    || !subFields.containsKey(layer.name)
                                    || subFields[layer.name].contains(fld)
                                    || subFields[layer.name].contains(fld.name)) {
                                attributes.put(fld.name, f.get(fld))
                            }
                        }
                    }
                    geometry.userData = attributes
                    // To Screen
                    MathTransform worldToScreenTransform = ProjectiveTransform.create(RendererUtilities.worldToScreenTransform(b.env, new Rectangle(0,0, tileSize, tileSize)))
                    geometry = JTS.transform(geometry, worldToScreenTransform)
                    //Encode
                    encoder.addFeature(layer.name, attributes, geometry)
                }
            })
        }

        encoder.encode()
    }

}

<|start_filename|>src/test/groovy/geoscript/style/ColorMapTest.groovy<|end_filename|>
package geoscript.style

import geoscript.filter.Color
import geoscript.layer.GeoTIFF
import geoscript.layer.Raster
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The ColorMap UnitTest
 * @author <NAME>
 */
class ColorMapTest {

    @Test void colorMap() {

        // Create a ColorMap from a List of Maps
        def colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])
        assertEquals "ramp", colorMap.type
        assertEquals 2, colorMap.values.size()
        assertFalse colorMap.extended

        colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]], "interval")
        assertEquals "interval", colorMap.type
        assertEquals 2, colorMap.values.size()
        assertFalse colorMap.extended

        colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]], "interval", true)
        assertEquals "interval", colorMap.type
        assertEquals 2, colorMap.values.size()
        assertTrue colorMap.extended

        // Load a Raster
        File file = new File(getClass().getClassLoader().getResource("raster.tif").toURI())
        GeoTIFF geoTIFF = new GeoTIFF(file)
        Raster raster = geoTIFF.read()
        Map extrema = raster.extrema
        double min = extrema.min[0]
        double max = extrema.max[0]

        // Create a ColorMap for a Raster and a color palette
        colorMap = new ColorMap(raster,"Greens", 5)
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 5, colorMap.values.size()
        assertEquals min, colorMap.values[0].quantity, 0.1
        assertEquals "#edf8e9", colorMap.values[0].color.hex
        assertEquals max, colorMap.values[4].quantity, 0.1
        assertEquals "#006d2c", colorMap.values[4].color.hex

        // Create a ColorMap for min and max values and a color palette
        colorMap = new ColorMap(min, max, "Greens", 5)
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 5, colorMap.values.size()
        assertEquals min, colorMap.values[0].quantity, 0.1
        assertEquals "#edf8e9", colorMap.values[0].color.hex
        assertEquals max, colorMap.values[4].quantity, 0.1
        assertEquals "#006d2c", colorMap.values[4].color.hex

        // Create a ColorMap for a Raster and a List of Colors
        colorMap = new ColorMap(raster, Color.getPaletteColors("Greens", 5))
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 5, colorMap.values.size()
        assertEquals min, colorMap.values[0].quantity, 0.1
        assertEquals "#edf8e9", colorMap.values[0].color.hex
        assertEquals max, colorMap.values[4].quantity, 0.1
        assertEquals "#006d2c", colorMap.values[4].color.hex

        // Create a ColorMap for a min and max value and a List of Colors
        colorMap = new ColorMap(min, max, Color.getPaletteColors("Greens", 5))
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 5, colorMap.values.size()
        assertEquals min, colorMap.values[0].quantity, 0.1
        assertEquals "#edf8e9", colorMap.values[0].color.hex
        assertEquals max, colorMap.values[4].quantity, 0.1
        assertEquals "#006d2c", colorMap.values[4].color.hex
    }

    @Test void apply() {
        def colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])
        def sym = Symbolizer.styleFactory.createRasterSymbolizer()
        colorMap.apply(sym)

        sym.colorMap.type = org.geotools.styling.ColorMap.TYPE_RAMP
        assertEquals colorMap.values.size(), sym.colorMap.colorMapEntries.length
        assertEquals colorMap.values[0].color, sym.colorMap.colorMapEntries[0].color.value
        assertEquals colorMap.values[0].quantity, sym.colorMap.colorMapEntries[0].quantity.value
        assertEquals colorMap.values[1].color, sym.colorMap.colorMapEntries[1].color.value
        assertEquals colorMap.values[1].quantity, sym.colorMap.colorMapEntries[1].quantity.value
    }

    @Test void prepare() {
        def colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        colorMap.prepare(rule)
        def sym = rule.symbolizers[0]

        sym.colorMap.type = org.geotools.styling.ColorMap.TYPE_RAMP
        assertEquals colorMap.values.size(), sym.colorMap.colorMapEntries.length
        assertEquals colorMap.values[0].color, sym.colorMap.colorMapEntries[0].color.value
        assertEquals colorMap.values[0].quantity, sym.colorMap.colorMapEntries[0].quantity.value
        assertEquals colorMap.values[1].color, sym.colorMap.colorMapEntries[1].color.value
        assertEquals colorMap.values[1].quantity, sym.colorMap.colorMapEntries[1].quantity.value
    }

    @Test void string() {
        def colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])
        assertEquals "ColorMap(values = [[color:#008000, quantity:70], [color:#663333, quantity:256]], type = ramp, extended = false)", colorMap.toString()
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/CartoFactoriesTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class CartoFactoriesTest {

    @Test
    void list() {
        assertFalse(CartoFactories.list().isEmpty())
    }

    @Test
    void findByName() {
        assertNotNull(CartoFactories.findByName("png"))
        assertNotNull(CartoFactories.findByName("jpeg"))
        assertNotNull(CartoFactories.findByName("pdf"))
        assertNotNull(CartoFactories.findByName("svg"))
        assertNull(CartoFactories.findByName("doc"))
    }

    @Test
    void findByMimeType() {
        assertNotNull(CartoFactories.findByMimeType("image/png"))
        assertNotNull(CartoFactories.findByMimeType("image/jpeg"))
        assertNotNull(CartoFactories.findByMimeType("application/pdf"))
        assertNotNull(CartoFactories.findByMimeType("application/svg"))
        assertNull(CartoFactories.findByMimeType("text/plain"))
    }
}


<|start_filename|>src/main/groovy/geoscript/layer/io/KmlWriter.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.layer.Layer
import org.geotools.kml.KML
import org.geotools.kml.KMLConfiguration
import org.geotools.xsd.Encoder

/**
 * Write a {@link geoscript.layer.Layer Layer}  to a KML InputStream, File, or String.
 * @author <NAME>
 */
class KmlWriter implements Writer {

    /**
     * Write the Layer to the KML OutputStream
     * @param layer The Layer
     * @param out The KML OutputStream
     */
    void write(Layer layer, OutputStream out) {
        Encoder encoder = new Encoder(new KMLConfiguration());
        encoder.indenting = true
        encoder.omitXMLDeclaration = true
        encoder.encode(layer.fs.features, KML.kml, out);
    }

    /**
     * Write the Layer to the KML File
     * @param layer The Layer
     * @param file The KML File
     */
    void write(Layer layer, File file) {
        FileOutputStream out = new FileOutputStream(file)
        write(layer, out)
        out.close()
    }

    /**
     * Write the Layer to a KML String
     * @param layer The Layer
     * @return A KML String
     */
    String write(Layer layer) {
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        write(layer, out);
        out.close()
        return out.toString()
    }
}

<|start_filename|>src/main/groovy/geoscript/index/STRtree.groovy<|end_filename|>
package geoscript.index

import org.locationtech.jts.index.strtree.STRtree as JtsSTRtree

/**
 * Create a SpatialIndex using the STR Tree spatial index.
 * <p><blockquote><pre>
 * def index = new STRtree()
 * index.insert(new Bounds(0,0,10,10), new Point(5,5))
 * index.insert(new Bounds(2,2,6,6), new Point(4,4))
 *
 * def results = index.query(new Bounds(1,1,5,5))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class STRtree extends SpatialIndex {
	
    /**
     * Create a SpatialIndex using the STR Tree spatial index.
     */
    STRtree() {
        super(new JtsSTRtree())
    }

}



<|start_filename|>src/main/groovy/geoscript/carto/TableItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Font
import java.awt.Color

/**
 * Add table to a cartographic document
 * @author <NAME>
 */
class TableItem extends Item {

    List<String> columns = []

    List<Map> rows = []

    RowStyle columnRowStyle = new RowStyle(
        backGroundColor: Color.decode("#696969"),
        font: new Font("Arial",  Font.BOLD, 14),
        textColor: Color.WHITE,
        strokeColor: Color.BLACK
    )

    RowStyle evenRowStyle = new RowStyle(
        backGroundColor: Color.decode("#DCDCDC"),
        font: new Font("Arial", Font.PLAIN, 12),
        textColor: Color.BLACK,
        strokeColor: Color.BLACK
    )

    RowStyle oddRowStyle = new RowStyle(
        backGroundColor: Color.WHITE,
        font: new Font("Arial", Font.PLAIN, 12),
        textColor: Color.BLACK,
        strokeColor: Color.BLACK
    )

    /**
     * Create table from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    TableItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the columns of the table
     * @param names The column names
     * @return The TableItem
     */
    TableItem columns(List<String> names) {
        this.columns.addAll(names)
        this
    }

    /**
     * Add a column to the table
     * @param name The column name
     * @return The TableItem
     */
    TableItem column(String name) {
        this.columns.add(name)
        this
    }

    /**
     * Add a row to the table
     * @param values A Map of values
     * @return The TableItem
     */
    TableItem row(Map values) {
        rows.add(values)
        this
    }

    private class RowStyle {
        Color backGroundColor = Color.WHITE
        Color textColor = Color.BLACK
        Font font = new Font("Arial", Font.PLAIN, 12)
        Color strokeColor = Color.BLACK

        @Override
        String toString() {
            return "background-color = ${backGroundColor}, text-color = ${textColor}, font = ${font}, stroke-color = ${strokeColor}"
        }
    }

    @Override
    String toString() {
        "TableItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, columns = ${columns}, rows = ${rows}, " +
                "column-style = (${columnRowStyle}), even-row-style = (${evenRowStyle}), odd-row-style = (${oddRowStyle}))"
    }

}


<|start_filename|>src/main/groovy/geoscript/style/NestedDirectoryStyleRepository.groovy<|end_filename|>
package geoscript.style

import org.apache.commons.io.FilenameUtils

/**
 * A StyleRepository that uses a nested directory structure.  There
 * is a directory for each layer which contains a file for each style.
 * @author <NAME>
 */
class NestedDirectoryStyleRepository implements StyleRepository {

    private final File directory

    NestedDirectoryStyleRepository(File directory) {
        this.directory = directory
    }

    @Override
    String getDefaultForLayer(String layerName) {
        getForLayer(layerName, layerName)
    }

    @Override
    String getForLayer(String layerName, String styleName) {
        File layerDirectory = new File(directory, layerName)
        if (layerDirectory.exists()) {
            ["sld", "css"].findResult { String ext ->
                File file = new File(layerDirectory, "${styleName}.${ext}")
                if (file.exists()) {
                    return file.text
                }
            }
        } else {
            ""
        }
    }

    @Override
    List<Map<String, String>> getForLayer(String layerName) {
        File layerDirectory = new File(directory, layerName)
        if (layerDirectory.exists()) {
            layerDirectory.listFiles(new StyleFileNameFilter()).collect { File file ->
                [
                        layerName: layerName,
                        styleName: FilenameUtils.getBaseName(file.name),
                        style    : file.text
                ]
            }
        } else {
            []
        }
    }

    @Override
    List<Map<String, String>> getAll() {
        List styles = []
        this.directory.listFiles(new DirectoryFileFilter()).each { File dir ->
            dir.listFiles(new StyleFileNameFilter()).each { File file ->
                styles.add([
                        layerName: dir.name,
                        styleName: FilenameUtils.getBaseName(file.name),
                        style    : file.text
                ])
            }
        }
        styles
    }

    @Override
    void save(String layerName, String styleName, String style, Map options = [:]) {
        File layerDirectory = new File(directory, layerName)
        layerDirectory.mkdir()
        File file = new File(layerDirectory, "${styleName}.${options.get('type','sld')}")
        file.text = style
    }

    @Override
    void delete(String layerName, String styleName) {
        File layerDirectory = new File(directory, layerName)
        if (layerDirectory.exists()) {
            ["sld", "css"].each { String ext ->
                File file = new File(layerDirectory, "${styleName}.${ext}")
                if (file.exists()) {
                    file.delete()
                }
            }
        }
    }

    private static class StyleFileNameFilter implements FilenameFilter {
        @Override
        boolean accept(File dir, String name) {
            name.endsWith(".sld") || name.endsWith(".css")
        }
    }

    private static class DirectoryFileFilter implements FileFilter {
        @Override
        boolean accept(File pathname) {
            pathname.isDirectory()
        }
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/TextItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Color
import java.awt.Font

/**
 * Add text to a cartographic document
 * @author <NAME>
 */
class TextItem extends Item {

    String text

    Color color = Color.BLACK

    Font font = new Font("Arial", Font.PLAIN, 12)

    HorizontalAlign horizontalAlign = HorizontalAlign.LEFT

    VerticalAlign verticalAlign = VerticalAlign.BOTTOM

    /**
     * Create  text from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    TextItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the text
     * @param text The text
     * @return The TextItem
     */
    TextItem text(String text) {
        this.text = text
        this
    }

    /**
     * Set the color
     * @param color The text color
     * @return The TextItem
     */
    TextItem color(Color color) {
        this.color = color
        this
    }

    /**
     * Set the font
     * @param font The font
     * @return The TextItem
     */
    TextItem font(Font font) {
        this.font = font
        this
    }

    /**
     * Set the horizontal alignment
     * @param horizontalAlign The HorizontalAlign
     * @return The TextItem
     */
    TextItem horizontalAlign(HorizontalAlign horizontalAlign) {
        this.horizontalAlign = horizontalAlign
        this
    }

    /**
     * Set the vertical alignment
     * @param verticalAlign The VerticalAlign
     * @return The TextItem
     */
    TextItem verticalAlign(VerticalAlign verticalAlign) {
        this.verticalAlign = verticalAlign
        this
    }

    @Override
    String toString() {
        "TextItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, text = ${text}, color = ${color}, font = ${font}, horizontal-align = ${horizontalAlign}, vertical-align = ${verticalAlign})"
    }
}


<|start_filename|>src/main/groovy/geoscript/index/HPRtree.groovy<|end_filename|>
package geoscript.index

import org.locationtech.jts.index.hprtree.HPRtree as JtsHPRtree

/**
 * Create a SpatialIndex using the Hilbert Packed Rtree (HPRtree) spatial index.
 * <p><blockquote><pre>
 * def index = new HPRtree()
 * index.insert(new Bounds(0,0,10,10), new Point(5,5))
 * index.insert(new Bounds(2,2,6,6), new Point(4,4))
 *
 * def results = index.query(new Bounds(1,1,5,5))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class HPRtree extends SpatialIndex {

    /**
     * Create a SpatialIndex using the HPR Tree spatial index.
     */
    HPRtree() {
        super(new JtsHPRtree())
    }

}



<|start_filename|>src/test/groovy/geoscript/geom/LinearRingTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The LinearRing unit test
 */
class LinearRingTest {

    @Test void constructors() {
        
        def l1 = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47], [111.0, -47]])
        assertEquals "LINEARRING (111 -47, 123 -48, 110 -47, 111 -47)", l1.wkt

        def l2 = new LinearRing([new Point(111.0, -47), new Point(123.0, -48), new Point(110.0, -47), new Point(111.0, -47)])
        assertEquals "LINEARRING (111 -47, 123 -48, 110 -47, 111 -47)", l2.wkt

        def l3 = new LinearRing([111.0, -47],[123.0, -48],[110.0, -47], [111.0, -47])
        assertEquals "LINEARRING (111 -47, 123 -48, 110 -47, 111 -47)", l3.wkt

        def l4 = new LinearRing(new Point(111.0, -47), new Point(123.0, -48), new Point(110.0, -47), new Point(111.0, -47))
        assertEquals "LINEARRING (111 -47, 123 -48, 110 -47, 111 -47)", l4.wkt
    }

    @Test void isClosed() {
        def ring = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47], [111.0, -47]])
        assertTrue(ring.isClosed())
    }

    @Test void isRing() {
        def ring = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47], [111.0, -47]])
        assertTrue(ring.isRing())
    }
}

<|start_filename|>src/test/groovy/geoscript/feature/SchemaTest.groovy<|end_filename|>
package geoscript.feature

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.*

/**
 * The Schema UniTest
 */
class SchemaTest {

    @Test void constructors() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        assertEquals "widgets geom: Point, name: String, price: Float", s1.toString()
        assertEquals "http://geoscript.org/feature", s1.uri

        Schema s2 = new Schema("widgets", [["geom","Point"], ["name","string"], ["price","float"]])
        assertEquals "widgets geom: Point, name: String, price: Float", s2.toString()
        assertEquals "http://geoscript.org/feature", s2.uri

        Schema s3 = new Schema("widgets", [[name: "geom",type: "Point"], [name: "name", type: "string"], [name: "price", type: "float"]])
        assertEquals "widgets geom: Point, name: String, price: Float", s3.toString()
        assertEquals "http://geoscript.org/feature", s3.uri

        Schema s4 = new Schema("widgets", "geom:Point:srid=4326,name:String,price:float")
        assertEquals "widgets geom: Point(EPSG:4326), name: String, price: Float", s4.toString()
        assertEquals "http://geoscript.org/feature", s4.uri

        Schema s5 = new Schema("widgets", "geom:Point:srid=4326,name:String,price:float", "http://geotools.org/feature")
        assertEquals "widgets geom: Point(EPSG:4326), name: String, price: Float", s5.toString()
        assertEquals "http://geotools.org/feature", s5.uri

        Schema s6 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")], "http://geotools.org/feature")
        assertEquals "widgets geom: Point, name: String, price: Float", s6.toString()
        assertEquals "http://geotools.org/feature", s6.uri
    }

    @Test void getName() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        assertEquals "widgets", s1.name
    }

    @Test void getGeom() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        Field field = s1.geom
        assertEquals "geom: Point", field.toString()

        Schema s2 = new Schema("widgets", [new Field("name","string"), new Field("price","float")])
        assertNull s2.geom
    }

    @Test void getProj() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point", "EPSG:4326"), new Field("name","string"), new Field("price","float")])
        assertEquals "EPSG:4326", s1.proj.id

        Schema s2 = new Schema("widgets", [new Field("name","string"), new Field("price","float")])
        assertNull s2.proj
    }

    @Test void field() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        assertEquals "geom: Point", s1.field("geom").toString()
        assertEquals "name: String", s1.field("name").toString()
        assertEquals "price: Float", s1.field("price").toString()
    }

    @Test void get() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        assertEquals "geom: Point", s1.get("geom").toString()
        assertEquals "name: String", s1.get("name").toString()
        assertEquals "price: Float", s1.get("price").toString()
    }

    @Test void getFields() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        List<Field> fields = s1.fields
        assertEquals 3, fields.size()
        assertEquals "geom: Point", fields[0].toString()
        assertEquals "name: String", fields[1].toString()
        assertEquals "price: Float", fields[2].toString()
    }

    @Test void has() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        assertTrue s1.has("geom")
        assertTrue s1.has("name")
        assertTrue s1.has("price")
        assertFalse s1.has("asdfasd")
        assertFalse s1.has("NOT A FIELD NAME")
        assertTrue s1.has(s1.get("geom"))
    }

    @Test void featureWithDefaultId() {
        Schema schema = new Schema("parcels", [new Field("geom","Polygon", "EPSG:4326"), new Field("number","string")])
        Feature feature = schema.feature([new Bounds(0,0,10,10).polygon, "123456"])
        assertNotNull feature.id
        assertTrue feature.id.length() > 10
        feature = schema.feature([geom: new Bounds(0,0,10,10).polygon, number: "123456"])
        assertNotNull feature.id
        assertTrue feature.id.length() > 10
    }

    @Test void feature() {
        Schema s1 = new Schema("houses", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        Feature f1 = s1.feature([new Point(111,-47), "House", 12.5],"house1")
        assertNotNull(f1)
        assertEquals "houses.house1 geom: POINT (111 -47), name: House, price: 12.5", f1.toString()

        Feature f2 = s1.feature(["geom": new Point(111,-47), "name": "House", "price": 12.5],"house1")
        assertNotNull(f2)
        assertEquals "houses.house1 geom: POINT (111 -47), name: House, price: 12.5", f2.toString()

        Feature f3 = s1.feature()
        assertNotNull(f3)
        assertNull f3.geom
        assertNull f3['name']
        assertNull f3['price']
    }

    @Test void toStringTest() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        assertEquals "widgets geom: Point, name: String, price: Float", s1.toString()
    }

    @Test void reproject() {
        Schema s1 = new Schema("houses", [new Field("geom","Point", "EPSG:2927"), new Field("name","string"), new Field("price","float")])
        assertEquals "houses geom: Point(EPSG:2927), name: String, price: Float", s1.toString()
        Schema s2 = s1.reproject("EPSG:4326","houses in lat/lon")
        assertEquals "houses in lat/lon geom: Point(EPSG:4326), name: String, price: Float", s2.toString()
    }

    @Test void reprojectWithNull() {
        Schema s1 = new Schema("houses", [new Field("geom","Point", "EPSG:2927"), new Field("name","string"), new Field("price","float")])
        assertEquals "houses geom: Point(EPSG:2927), name: String, price: Float", s1.toString()
        Schema s2 = s1.reproject(null,"houses")
        assertEquals "houses geom: Point, name: String, price: Float", s2.toString()
        assertNull s2.geom.proj
    }

    @Test void changeGeometryType() {
        Schema s1 = new Schema("houses", [new Field("geom","Polygon", "EPSG:2927"), new Field("name","string"), new Field("price","float")])
        assertEquals "houses geom: Polygon(EPSG:2927), name: String, price: Float", s1.toString()
        Schema s2 = s1.changeGeometryType("Polygon","houses buffered")
        assertEquals "houses buffered geom: Polygon(EPSG:2927), name: String, price: Float", s2.toString()
    }

    @Test void addField() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String")])
        assertEquals "points geom: Point(EPSG:4326), name: String", s1.toString()
        Schema s2 = s1.addField(new Field("description","String"), "points_with_description")
        assertEquals "points_with_description geom: Point(EPSG:4326), name: String, description: String", s2.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String", s1.toString()
    }

    @Test void addFields() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String")])
        assertEquals "points geom: Point(EPSG:4326), name: String", s1.toString()
        Schema s2 = s1.addFields([
            new Field("x","double"),
            new Field("y","double")],
            "points_with_xy"
        )
        assertEquals "points_with_xy geom: Point(EPSG:4326), name: String, x: Double, y: Double", s2.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String", s1.toString()
    }

    @Test void removeField() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String"), new Field("x", "Double"), new Field("y", "Double")])
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
        Schema s2 = s1.removeField(s1.get("name"), "points_without_name")
        assertEquals "points_without_name geom: Point(EPSG:4326), x: Double, y: Double", s2.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
    }

    @Test void removeFields() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String"), new Field("x", "Double"), new Field("y", "Double")])
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
        Schema s2 = s1.removeFields([s1.get("x"), s1.get("y")], "points_without_xy")
        assertEquals "points_without_xy geom: Point(EPSG:4326), name: String", s2.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
    }

    @Test void changeField() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String"), new Field("x", "Double"), new Field("y", "Double")])
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
        Schema s2 = s1.changeField(s1.get("name"), new Field("id","int"), "points_with_id")
        assertEquals "points_with_id geom: Point(EPSG:4326), id: Integer, x: Double, y: Double", s2.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
    }

    @Test void changeFields() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String"), new Field("x", "Double"), new Field("y", "Double")])
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
        Schema s2 = s1.changeFields([
            (s1.get("geom")): new Field("geom","Polygon", "EPSG:2927"),
            (s1.get("name")): new Field("id","int")
        ], "points_new")
        assertEquals "points_new geom: Polygon(EPSG:2927), id: Integer, x: Double, y: Double", s2.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
    }

    @Test void equalsAndHashCode() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String")])
        Schema s2 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String")])
        Schema s3 = new Schema("facilities", [new Field("geom","Point","EPSG:4326"), new Field("name","String")])
        Schema s4 = new Schema("hours", [new Field("the_geom","Polygon","EPSG:2927"), new Field("address","String")])
        // Equals
        assertTrue s1.equals(s2)
        assertFalse s1.equals(s3)
        assertFalse s1.equals(s4)
        assertFalse s2.equals(s3)
        assertFalse s2.equals(s4)
        assertFalse s3.equals(s4)
        // Hashcode
        assertTrue s1.hashCode() == s2.hashCode()
        assertFalse s1.hashCode() == s3.hashCode()
        assertFalse s1.hashCode() == s4.hashCode()
        assertFalse s2.hashCode() == s3.hashCode()
        assertFalse s2.hashCode() == s4.hashCode()
        assertFalse s3.hashCode() == s4.hashCode()
    }

    @Test void addSchema() {
        Schema s1 = new Schema("points", [new Field("geom", "Point", "EPSG:4326"), new Field("id","int"), new Field("name","String")])
        Schema s2 = new Schema("houses", [new Field("geom", "Point", "EPSG:4326"), new Field("id","int"), new Field("description","String"), new Field("price","Double")])

        // No prefix all, No duplicates,
        Map result = s1.addSchema(s2, "points_houses")
        Schema s3 = result.schema
        assertEquals "points_houses geom: Point(EPSG:4326), id: Integer, name: String, description: String, price: Double", s3.toString()
        assertEquals "geom", result.fields[0]["geom"]
        assertEquals "id", result.fields[0]["id"]
        assertEquals "name", result.fields[0]["name"]
        assertEquals "description", result.fields[1]["description"]
        assertEquals "price", result.fields[1]["price"]

        // Postfix all, No duplicates
        result = s1.addSchema(s2, "points_houses", postfixAll: true, includeDuplicates: false)
        s3 = result.schema
        assertEquals "points_houses geom: Point(EPSG:4326), id1: Integer, name1: String, id2: Integer, description2: String, price2: Double", s3.toString()
        assertEquals "geom", result.fields[0]["geom"]
        assertEquals "id1", result.fields[0]["id"]
        assertEquals "name1", result.fields[0]["name"]
        assertEquals "id2", result.fields[1]["id"]
        assertEquals "description2", result.fields[1]["description"]
        assertEquals "price2", result.fields[1]["price"]

        // No postfix all, Duplicates
        result = s1.addSchema(s2, "points_houses", postfixAll: false, includeDuplicates: true)
        s3 = result.schema
        assertEquals "points_houses geom: Point(EPSG:4326), id: Integer, name: String, id2: Integer, description: String, price: Double", s3.toString()
        assertEquals "geom", result.fields[0]["geom"]
        assertEquals "id", result.fields[0]["id"]
        assertEquals "name", result.fields[0]["name"]
        assertEquals "id2", result.fields[1]["id"]
        assertEquals "description", result.fields[1]["description"]
        assertEquals "price", result.fields[1]["price"]

        // Prefix all, Duplicates
        result = s1.addSchema(s2, "points_houses", postfixAll: true, includeDuplicates: true)
        s3 = result.schema
        assertEquals "points_houses geom: Point(EPSG:4326), id1: Integer, name1: String, id2: Integer, description2: String, price2: Double", s3.toString()
        assertEquals "geom", result.fields[0]["geom"]
        assertEquals "id1", result.fields[0]["id"]
        assertEquals "name1", result.fields[0]["name"]
        assertEquals "id2", result.fields[1]["id"]
        assertEquals "description2", result.fields[1]["description"]
        assertEquals "price2", result.fields[1]["price"]

        // Max Field name length (shapefiles)
        result = s1.addSchema(s2, "points_houses", maxFieldNameLength: 10, postfixAll: true)
        s3 = result.schema
        assertEquals "points_houses geom: Point(EPSG:4326), id1: Integer, name1: String, id2: Integer, descriptio2: String, price2: Double", s3.toString()
        assertEquals "geom", result.fields[0]["geom"]
        assertEquals "id1", result.fields[0]["id"]
        assertEquals "name1", result.fields[0]["name"]
        assertEquals "id2", result.fields[1]["id"]
        assertEquals "descriptio2", result.fields[1]["description"]
        assertEquals "price2", result.fields[1]["price"]

        // Postfix all, custom postfixes
        result = s1.addSchema(s2, "points_houses", postfixAll: true, firstPostfix: 'A', secondPostfix: 'B')
        s3 = result.schema
        assertEquals "points_houses geom: Point(EPSG:4326), idA: Integer, nameA: String, idB: Integer, descriptionB: String, priceB: Double", s3.toString()
        assertEquals "geom", result.fields[0]["geom"]
        assertEquals "idA", result.fields[0]["id"]
        assertEquals "nameA", result.fields[0]["name"]
        assertEquals "idB", result.fields[1]["id"]
        assertEquals "descriptionB", result.fields[1]["description"]
        assertEquals "priceB", result.fields[1]["price"]
    }

    @Test void includeFields() {
        Schema s1 = new Schema("points", [new Field("geom","Point","EPSG:4326"), new Field("name","String"), new Field("x", "Double"), new Field("y", "Double")])
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
        Schema s2 = s1.includeFields([s1.get("geom"), s1.get("name")], "points_new")
        assertEquals "points_new geom: Point(EPSG:4326), name: String", s2.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
        Schema s3 = s1.includeFields(["geom","x","y"], "points_new")
        assertEquals "points_new geom: Point(EPSG:4326), x: Double, y: Double", s3.toString()
        assertEquals "points geom: Point(EPSG:4326), name: String, x: Double, y: Double", s1.toString()
    }

    @Test void lookUpAlias() {
        assertEquals "String", Schema.lookUpAlias("java.lang.String")
        assertEquals "Double", Schema.lookUpAlias("java.lang.Double")
        assertEquals "CircularRing", Schema.lookUpAlias("org.geotools.geometry.jts.CircularRing")
        assertEquals "CircularString", Schema.lookUpAlias("org.geotools.geometry.jts.CircularString")
        assertEquals "CompoundRing", Schema.lookUpAlias("org.geotools.geometry.jts.CompoundRing")
        assertEquals "CompoundCurve", Schema.lookUpAlias("org.geotools.geometry.jts.CompoundCurve")
    }

    @Test void lookUpBinding() {
        assertEquals "java.lang.String", Schema.lookUpBinding("String")
        assertEquals "java.lang.Double", Schema.lookUpBinding("Double")
        assertEquals "org.geotools.geometry.jts.CircularRing", Schema.lookUpBinding("CircularRing")
        assertEquals "org.geotools.geometry.jts.CircularString", Schema.lookUpBinding("CircularString")
        assertEquals "org.geotools.geometry.jts.CompoundRing", Schema.lookUpBinding("CompoundRing")
        assertEquals "org.geotools.geometry.jts.CompoundCurve", Schema.lookUpBinding("CompoundCurve")
    }

    @Test void isValidFieldType() {
        assertTrue(Schema.isValidFieldType("str"))
        assertTrue(Schema.isValidFieldType("string"))
        assertTrue(Schema.isValidFieldType("java.lang.String"))
        assertTrue(Schema.isValidFieldType("Point"))
        assertTrue(Schema.isValidFieldType("geoscript.geom.Point"))
        assertTrue(Schema.isValidFieldType("int"))
        assertTrue(Schema.isValidFieldType("integer"))

        assertFalse(Schema.isValidFieldType("asdf"))
        assertFalse(Schema.isValidFieldType("junk"))
        assertFalse(Schema.isValidFieldType("inter"))
    }

    @Test void spec() {
        Schema s1 = new Schema("widgets", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        assertEquals "geom:Point,name:String,price:Float", s1.spec
        Schema s2 = new Schema("widgets", [new Field("geom","Point","EPSG:2927"), new Field("name","string"), new Field("price","float")])
        assertEquals "geom:Point:srid=2927,name:String,price:Float", s2.spec
    }
}



<|start_filename|>src/main/groovy/geoscript/feature/io/KmlReader.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Feature
import org.geotools.kml.KMLConfiguration
import org.geotools.xsd.Parser

/**
 * Read a Feature from a KML Placemark
 * @author <NAME>
 */
class KmlReader implements Reader {

    /**
     * Read a Feature from a KML Placemark
     * @param options The named parameters
     * <ul>
     *     <li>subFields = A List of fields to include (defaults to Geometry, name, and description)</li>
     * </ul>
     * @param str The KML String
     * @return A Feature
     */
    Feature read(Map options = [:], String str) {
        List subFields = options.get("subFields", ["Geometry", "name", "description"])
        Parser parser = new Parser(new KMLConfiguration())
        Feature feature = new Feature(parser.parse(new StringReader(str)))
        feature.schema.includeFields(subFields, feature.schema.name).feature(feature)
    }
}


<|start_filename|>src/main/groovy/geoscript/workspace/Database.groovy<|end_filename|>
package geoscript.workspace

import groovy.sql.Sql
import org.geotools.data.DataStore
import org.geotools.jdbc.JDBCDataStore
import org.geotools.jdbc.VirtualTable
import geoscript.feature.Field
import geoscript.layer.Layer
import org.geotools.jdbc.VirtualTableParameter

import javax.sql.DataSource

/**
 * A Workspace that is a Database.
 * <p>A Database subclass can add a SQL query as a Layer:</p>
 * <p><blockquote><pre>
 * Database db = new H2("acme", "target/h2")
 * Layer statesLayer = h2.add(shp, 'states')
 * String sql = """select st_centroid("the_geom") as "the_geom", "STATE_NAME" FROM "states""""
 * Layer statesCentroidLayer = h2.createView("states_centroids", sql, new Field("the_geom", "Point", "EPSG:4326"))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Database extends Workspace {

    /**
     * Create a new Database wrapping a GeoTools JDBCDataStore
     * @param The GeoTools DataStore
     */
    Database(DataStore ds) {
        super(ds)
    }

    /**
     * Get the javax.sql.DataSource
     * @return The javax.sql.DataSource
     */
    DataSource getDataSource() {
        (ds as JDBCDataStore).dataSource
    }

    /**
     * Get a groovy.sql.Sql object that provides direct access
     * to the underlying database
     * @return A groovy.sql.Sql object
     */
    Sql getSql() {
        new Sql((ds as JDBCDataStore).dataSource)
    }

    /**
     * Remove the layer from the database
     * @param layerName The layer name
     */
    void remove(String layerName) {
        (ds as JDBCDataStore).removeSchema(layerName)
    }

    /**
     * Create a Layer from a SQL View
     * @param options The named parameters
     * <ul>
     *     <li>params = The query parameters</li>
     *     <li>primaryKeyFields = The list of primary key fields</li>
     * </ul>
     * @param layerName The layer name
     * @param sql The SQL
     * @param geometryField The geometry Field
     * @return The new Layer
     */
    Layer createView(Map options = [:], String layerName, String sql, Field geometryField) {
        addVirtualTable(options, layerName, sql, geometryField)
        get(layerName)
    }

    /**
     * Delete a SQL View Layer
     * @param name The name of the SQL View Layer
     */
    void deleteView(String name) {
        (ds as JDBCDataStore).dropVirtualTable(name)
    }

    /**
     * Create an index
     * @param layerName The layer or table name
     * @param indexName The index name
     * @param fieldName The field name
     * @param unique Whether the index is unique or not
     */
    void createIndex(String layerName, String indexName, String fieldName, boolean unique) {
        createIndex(layerName, indexName, [fieldName], unique)
    }

    /**
     * Create an index
     * @param layerName The layer or table name
     * @param indexName The index name
     * @param fieldNames A List of field names
     * @param unique Whether the index is unique or not
     */
    void createIndex(String layerName, String indexName, List<String> fieldNames, boolean unique) {
        (ds as JDBCDataStore).createIndex(new org.geotools.jdbc.Index(layerName, indexName, unique, fieldNames as String[]))
    }

    /**
     * Delete an index
     * @param layerName The layer or table name
     * @param indexName The index name
     */
    void deleteIndex(String layerName, String indexName) {
        (ds as JDBCDataStore).dropIndex(layerName, indexName)
    }

    /**
     * Get a List of indexes for a layer or table by name
     * @param layerName The layer or table name
     * @return A List of indexes
     */
    List getIndexes(String layerName) {
        (ds as JDBCDataStore).getIndexes(layerName).collect{ [
            name: it.indexName,
            unique: it.unique,
            attributes: it.attributes
        ]}
    }

    /**
     * Create and add a virtual table to the JDBC based DataStore
     * @param options The named parameters
     * <ul>
     *     <li>params = A Map of query parameters</li>
     *     <li>primaryKeyFields = A List of primary key fields</li>
     * </ul>
     * @param layerName The layer name
     * @param sql The SQL
     * @param geometryField The geometry Field
     */
    private void addVirtualTable(Map options = [:], String layerName, String sql, Field geometryField) {
        // Named parameters
        def params = options.get("params")
        List primaryKeyFields = options.get("primaryKeyFields")
        // Build the VirtualTable
        VirtualTable vt = new VirtualTable(layerName, sql)
        if (params) {
            if (!(params instanceof List)) {
                params = [params]
            }
            params.each {p ->
                if (p instanceof List) {
                    vt.addParameter(new VirtualTableParameter(p[0], p[1]))
                } else {
                    vt.addParameter(new VirtualTableParameter(p))
                }
            }
        }
        if (geometryField != null) {
            vt.addGeometryMetadatata(geometryField.name, getGeometryClass(geometryField.typ), geometryField.proj.id.replaceAll("EPSG:","") as int)
        }
        if (primaryKeyFields != null && primaryKeyFields.size() > 0) {
            vt.setPrimaryKeyColumns(primaryKeyFields[0] instanceof Field ? primaryKeyFields.collect{fld->fld.name} : primaryKeyFields)
        }
        (ds as JDBCDataStore).createVirtualTable(vt)
    }

    /**
     * Get the JTS Geometry Class from a String (Point, MultiPoint,
     * LineString, MultiLineString, LinearRing, Polygon, MultiPolygon,
     * and GeometryCollection are all valid).
     * @param geometry The geometry type
     * @return The JTS Geometry Class
     */
    private Class getGeometryClass(String geometry) {
        if (geometry.equalsIgnoreCase("point")) {
            return org.locationtech.jts.geom.Point.class
        } else if (geometry.equalsIgnoreCase("multipoint")) {
            return org.locationtech.jts.geom.MultiPoint.class
        } else if (geometry.equalsIgnoreCase("linestring")) {
            return org.locationtech.jts.geom.LineString.class
        } else if (geometry.equalsIgnoreCase("multilinestring")) {
            return org.locationtech.jts.geom.MultiLineString.class
        } else if (geometry.equalsIgnoreCase("linearring")) {
            return org.locationtech.jts.geom.LinearRing.class
        } else if (geometry.equalsIgnoreCase("polygon")) {
            return org.locationtech.jts.geom.Polygon.class
        } else if (geometry.equalsIgnoreCase("multipolygon")) {
            return org.locationtech.jts.geom.MultiPolygon.class
        } else if (geometry.equalsIgnoreCase("geometrycollection")) {
            return org.locationtech.jts.geom.GeometryCollection.class
        } else {
            return org.locationtech.jts.geom.Geometry.class
        }
    }
}



<|start_filename|>src/test/groovy/geoscript/plot/PieTest.groovy<|end_filename|>
package geoscript.plot

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import static org.junit.jupiter.api.Assertions.*

/**
 * The Pie Unit Test
 * @author <NAME>
 */
class PieTest {

    @TempDir
    File folder

    @Test void pie() {
        Map data = [
            "A":20,"B":45,"C":2,"D":14
        ]
        Chart chart = Pie.pie(data)
        File file = new File(folder,"pie.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void pie3d() {
        Map data = [
                "A":20,"B":45,"C":2,"D":14
        ]
        Chart chart = Pie.pie(data, trid: true)
        File file = new File(folder,"pie3d.png")
        chart.save(file)
        assertTrue(file.exists())
    }

}


<|start_filename|>src/test/groovy/geoscript/style/io/ColorTableReaderTest.groovy<|end_filename|>
package geoscript.style.io

import geoscript.style.ColorMap
import geoscript.style.Symbolizer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir


import static org.junit.jupiter.api.Assertions.*

/**
 * The ColorTableReader Unit Test
 * @author <NAME>
 */
class ColorTableReaderTest {

    @TempDir
    File folder

    @Test void readColorTableFromString() {

        Reader reader = new ColorTableReader()

        // Semi-colon separated Color values
        String str = """0  255:255:255
2  255:255:0
5  0:255:0
10 0:255:255
15 0:0:255
30 255:0:255
50 255:0:0
90 0:0:0
"""
        Symbolizer sym = reader.read(str)
        assertTrue sym instanceof ColorMap
        ColorMap colorMap = sym as ColorMap
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 8, colorMap.values.size()
        assertEquals 0, colorMap.values[0].quantity as int
        assertEquals "#ffffff", colorMap.values[0].color.hex
        assertEquals 2, colorMap.values[1].quantity as int
        assertEquals "#ffff00", colorMap.values[1].color.hex
        assertEquals 5, colorMap.values[2].quantity as int
        assertEquals "#00ff00", colorMap.values[2].color.hex
        assertEquals 10, colorMap.values[3].quantity as int
        assertEquals "#00ffff", colorMap.values[3].color.hex
        assertEquals 15, colorMap.values[4].quantity as int
        assertEquals "#0000ff", colorMap.values[4].color.hex
        assertEquals 30, colorMap.values[5].quantity as int
        assertEquals "#ff00ff", colorMap.values[5].color.hex
        assertEquals 50, colorMap.values[6].quantity as int
        assertEquals "#ff0000", colorMap.values[6].color.hex
        assertEquals 90, colorMap.values[7].quantity as int
        assertEquals "#000000", colorMap.values[7].color.hex

        // Space delimited color values
        str = """0 46 154 88
1800 251 255 128
2800 224 108 31
3500 200 55 55
4000 215 244 244"""
        sym = reader.read(str)
        assertTrue sym instanceof ColorMap
        colorMap = sym as ColorMap
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 5, colorMap.values.size()
        assertEquals 0, colorMap.values[0].quantity as int
        assertEquals "#2e9a58", colorMap.values[0].color.hex
        assertEquals 1800, colorMap.values[1].quantity as int
        assertEquals "#fbff80", colorMap.values[1].color.hex
        assertEquals 2800, colorMap.values[2].quantity as int
        assertEquals "#e06c1f", colorMap.values[2].color.hex
        assertEquals 3500, colorMap.values[3].quantity as int
        assertEquals "#c83737", colorMap.values[3].color.hex
        assertEquals 4000, colorMap.values[4].quantity as int
        assertEquals "#d7f4f4", colorMap.values[4].color.hex

        // Named colors
        str = """0 white
1 yellow
90 green
180 cyan
270 red
360 yellow"""
        sym = reader.read(str)
        assertTrue sym instanceof ColorMap
        colorMap = sym as ColorMap
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 6, colorMap.values.size()
        assertEquals 0, colorMap.values[0].quantity as int
        assertEquals "#ffffff", colorMap.values[0].color.hex
        assertEquals 1, colorMap.values[1].quantity as int
        assertEquals "#ffff00", colorMap.values[1].color.hex
        assertEquals 90, colorMap.values[2].quantity as int
        assertEquals "#008000", colorMap.values[2].color.hex
        assertEquals 180, colorMap.values[3].quantity as int
        assertEquals "#00ffff", colorMap.values[3].color.hex
        assertEquals 270, colorMap.values[4].quantity as int
        assertEquals "#ff0000", colorMap.values[4].color.hex
        assertEquals 360, colorMap.values[5].quantity as int
        assertEquals "#ffff00", colorMap.values[5].color.hex
    }

    @Test void readColorTableFromFile() {

        Reader reader = new ColorTableReader()

        File file = new File(folder,"colortable.txt")
        file.write("""1 black
2 white
3 red""")
        Symbolizer sym = reader.read(file)
        assertTrue sym instanceof ColorMap
        ColorMap colorMap = sym as ColorMap
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 3, colorMap.values.size()
        assertEquals 1, colorMap.values[0].quantity as int
        assertEquals "#000000", colorMap.values[0].color.hex
        assertEquals 2, colorMap.values[1].quantity as int
        assertEquals "#ffffff", colorMap.values[1].color.hex
        assertEquals 3, colorMap.values[2].quantity as int
        assertEquals "#ff0000", colorMap.values[2].color.hex
    }

    @Test void readColorTableFromInputStream() {

        Reader reader = new ColorTableReader()

        File file = new File(folder,"colortable.txt")
        file.write("""1 black
2 white
3 red""")
        Symbolizer sym = reader.read(new FileInputStream(file))
        assertTrue sym instanceof ColorMap
        ColorMap colorMap = sym as ColorMap
        assertEquals "ramp", colorMap.type
        assertFalse colorMap.extended
        assertEquals 3, colorMap.values.size()
        assertEquals 1, colorMap.values[0].quantity as int
        assertEquals "#000000", colorMap.values[0].color.hex
        assertEquals 2, colorMap.values[1].quantity as int
        assertEquals "#ffffff", colorMap.values[1].color.hex
        assertEquals 3, colorMap.values[2].quantity as int
        assertEquals "#ff0000", colorMap.values[2].color.hex
    }
}

<|start_filename|>src/main/groovy/geoscript/carto/io/JsonCartoReader.groovy<|end_filename|>
package geoscript.carto.io

import geoscript.carto.CartoBuilder
import geoscript.carto.CartoFactories
import geoscript.carto.DateTextItem
import geoscript.carto.GridItem
import geoscript.carto.HorizontalAlign
import geoscript.carto.ImageItem
import geoscript.carto.LegendItem
import geoscript.carto.LineItem
import geoscript.carto.MapItem
import geoscript.carto.NorthArrowItem
import geoscript.carto.NorthArrowStyle
import geoscript.carto.OverviewMapItem
import geoscript.carto.PageSize
import geoscript.carto.ParagraphItem
import geoscript.carto.RectangleItem
import geoscript.carto.ScaleBarItem
import geoscript.carto.ScaleTextItem
import geoscript.carto.TableItem
import geoscript.carto.TextItem
import geoscript.carto.VerticalAlign
import geoscript.filter.Color
import geoscript.render.io.JsonMapReader
import groovy.json.JsonSlurper

import java.awt.Font
import java.text.DateFormat
import java.text.SimpleDateFormat

/**
 * Read a CartoBuilder from a JSON String
 * @author <NAME>
 */
class JsonCartoReader implements CartoReader {

    @Override
    String getName() {
        "json"
    }

    @Override
    CartoBuilder read(String str) {
        JsonSlurper jsonSlurper = new JsonSlurper()
        Map config = jsonSlurper.parseText(str)

        JsonMapReader mapReader = new JsonMapReader()
        Map mapItems = [:]
        CartoBuilder cartoBuilder = CartoFactories.findByName(config.type)
            .create(new PageSize(config.width, config.height))
        config.items.each { Map item ->
            String itemType = item.type
            if (itemType.equalsIgnoreCase("rectangle")) {
                RectangleItem rectangleItem = new RectangleItem(
                    item.x, item.y, item.width, item.height
                )
                if (item.strokeColor) {
                    rectangleItem.strokeColor(new Color(item.strokeColor).asColor())
                }
                if (item.fillColor) {
                    rectangleItem.fillColor(new Color(item.fillColor).asColor())
                }
                if (item.strokeWidth) {
                    rectangleItem.strokeWidth(item.strokeWidth as float)
                }
                cartoBuilder.rectangle(rectangleItem)
            } else if (itemType.equalsIgnoreCase("dateText")) {
                DateTextItem dateTextItem = new DateTextItem(
                    item.x, item.y, item.width, item.height
                )
                if (item.format) {
                    dateTextItem.format(item.format)
                }
                if (item.date) {
                    DateFormat dateFormat = new SimpleDateFormat(dateTextItem.format)
                    dateTextItem.date(dateFormat.parse(item.date))
                }
                if (item.color) {
                    dateTextItem.color(new Color(item.color).asColor())
                }
                if (item.font) {
                    dateTextItem.font(getFont(item.font))
                }
                if (item.horizontalAlign) {
                    dateTextItem.horizontalAlign(HorizontalAlign.valueOf(item.horizontalAlign.toString().toUpperCase()))
                }
                if (item.verticalAlign) {
                    dateTextItem.verticalAlign(VerticalAlign.valueOf(item.verticalAlign.toString().toUpperCase()))
                }
                cartoBuilder.dateText(dateTextItem)
            } else if (itemType.equalsIgnoreCase("image")) {
                ImageItem imageItem = new ImageItem(item.x, item.y, item.width, item.height)
                imageItem.path(new File(item.path).absoluteFile)
                cartoBuilder.image(imageItem)
            } else if (itemType.equalsIgnoreCase("line")) {
                LineItem lineItem = new LineItem(item.x, item.y, item.width, item.height)
                if (item.strokeColor) {
                    lineItem.strokeColor(new Color(item.strokeColor).asColor())
                }
                if (item.strokeWidth) {
                    lineItem.strokeWidth(item.strokeWidth as float)
                }
                cartoBuilder.line(lineItem)
            } else if (itemType.equalsIgnoreCase("map")) {
                MapItem mapItem = new MapItem(item.x, item.y, item.width, item.height)
                mapItem.map(mapReader.read(item))
                mapItems[item.name] = mapItem
                cartoBuilder.map(mapItem)
            } else if (itemType.equalsIgnoreCase("overViewMap")) {
                OverviewMapItem overviewMapItem = new OverviewMapItem(item.x, item.y, item.width, item.height)
                overviewMapItem.overviewMap(mapReader.read(item))
                overviewMapItem.linkedMap(mapItems[item.linkedMap].map)
                if (item.zoomIntoBounds) {
                    overviewMapItem.zoomIntoBounds(item.zoomIntoBounds)
                }
                if (item.scaleFactor) {
                    overviewMapItem.scaleFactor(item.scaleFactor)
                }
                cartoBuilder.overViewMap(overviewMapItem)
            } else if (itemType.equalsIgnoreCase("northArrow")) {
                NorthArrowItem northArrowItem = new NorthArrowItem(item.x, item.y, item.width, item.height)
                if (item.style) {
                    northArrowItem.style(NorthArrowStyle.valueOf(item.style.toString()))
                }
                if (item.fillColor1) {
                    northArrowItem.fillColor1(new Color(item.fillColor1).asColor())
                }
                if (item.fillColor2) {
                    northArrowItem.fillColor2(new Color(item.fillColor2).asColor())
                }
                if (item.strokeColor1) {
                    northArrowItem.strokeColor1(new Color(item.strokeColor1).asColor())
                }
                if (item.strokeColor2) {
                    northArrowItem.strokeColor2(new Color(item.strokeColor2).asColor())
                }
                if (item.strokeWidth) {
                    northArrowItem.strokeWidth(item.strokeWidth as float)
                }
                if (item.drawText != null) {
                    northArrowItem.drawText(item.drawText)
                }
                if (item.font) {
                    northArrowItem.font(getFont(item.font))
                }
                if (item.textColor) {
                    northArrowItem.textColor(new Color(item.textColor).asColor())
                }
                cartoBuilder.northArrow(northArrowItem)
            } else if (itemType.equalsIgnoreCase("text")) {
                TextItem textItem = new TextItem(item.x, item.y, item.width, item.height)
                textItem.text(item.text)
                if (item.color) {
                    textItem.color(new Color(item.color).asColor())
                }
                if (item.font) {
                    textItem.font(getFont(item.font))
                }
                if (item.horizontalAlign) {
                    textItem.horizontalAlign(HorizontalAlign.valueOf(item.horizontalAlign.toString().toUpperCase()))
                }
                if (item.verticalAlign) {
                    textItem.verticalAlign(VerticalAlign.valueOf(item.verticalAlign.toString().toUpperCase()))
                }
                cartoBuilder.text(textItem)
            } else if (itemType.equalsIgnoreCase("paragraph")) {
                ParagraphItem paragraphItem = new ParagraphItem(item.x, item.y, item.width, item.height)
                paragraphItem.text(item.text)
                if (item.color) {
                    paragraphItem.color(new Color(item.color).asColor())
                }
                if (item.font) {
                    paragraphItem.font(getFont(item.font))
                }
                cartoBuilder.paragraph(paragraphItem)
            } else if (itemType.equalsIgnoreCase("scaleText")) {
                ScaleTextItem scaleTextItem = new ScaleTextItem(item.x, item.y, item.width, item.height)
                scaleTextItem.map(mapItems[item.map].map)
                if (item.format) {
                    scaleTextItem.format(item.format)
                }
                if (item.prefixText) {
                    scaleTextItem.prefixText(item.prefixText)
                }
                if (item.color) {
                    scaleTextItem.color(new Color(item.color).asColor())
                }
                if (item.font) {
                    scaleTextItem.font(getFont(item.font))
                }
                if (item.horizontalAlign) {
                    scaleTextItem.horizontalAlign(HorizontalAlign.valueOf(item.horizontalAlign.toString().toUpperCase()))
                }
                if (item.verticalAlign) {
                    scaleTextItem.verticalAlign(VerticalAlign.valueOf(item.verticalAlign.toString().toUpperCase()))
                }
                cartoBuilder.scaleText(scaleTextItem)
            } else if (itemType.equalsIgnoreCase("scaleBar")) {
                ScaleBarItem scaleBarItem = new ScaleBarItem(item.x, item.y, item.width, item.height)
                scaleBarItem.map(mapItems[item.map].map)
                if (item.strokeColor) {
                    scaleBarItem.strokeColor(new Color(item.strokeColor).asColor())
                }
                if (item.fillColor) {
                    scaleBarItem.fillColor(new Color(item.fillColor).asColor())
                }
                if (item.font) {
                    scaleBarItem.font(getFont(item.font))
                }
                if (item.strokeWidth) {
                    scaleBarItem.strokeWidth(item.strokeWidth as float)
                }
                if (item.border) {
                    scaleBarItem.border(item.border as int)
                }
                if (item.units) {
                    scaleBarItem.units(ScaleBarItem.Units.valueOf(item.units.toString().toUpperCase()))
                }
                cartoBuilder.scaleBar(scaleBarItem)
            } else if (itemType.equalsIgnoreCase("grid")) {
                GridItem gridItem = new GridItem(item.x, item.y, item.width, item.height)
                if (item.size) {
                    gridItem.size(item.size as int)
                }
                if (item.strokeWidth) {
                    gridItem.strokeWidth(item.strokeWidth as float)
                }
                if (item.strokeColor) {
                    gridItem.strokeColor(new Color(item.strokeColor).asColor())
                }
                cartoBuilder.grid(gridItem)
            } else if (itemType.equalsIgnoreCase("table")) {
                TableItem tableItem = new TableItem(item.x, item.y, item.width, item.height)
                tableItem.columns(item.columns.collect { it })
                item.rows.each { Map row ->
                    tableItem.row(row)
                }
                cartoBuilder.table(tableItem)
            } else if (itemType.equalsIgnoreCase("legend")) {
                LegendItem legendItem = new LegendItem(item.x, item.y, item.width, item.height)
                legendItem.addMap(mapItems[item.map].map)
                if (item.backgroundColor) {
                    legendItem.backgroundColor(new Color(item.backgroundColor).asColor())
                }
                if (item.title) {
                    legendItem.title(item.title)
                }
                if (item.titleFont) {
                    legendItem.titleFont(getFont(item.titleFont))
                }
                if (item.titleColor) {
                    legendItem.titleColor(new Color(item.titleColor).asColor())
                }
                if (item.textFont) {
                    legendItem.textFont(getFont(item.textFont))
                }
                if (item.textColor) {
                    legendItem.textColor(new Color(item.textColor).asColor())
                }
                if (item.numberFormat) {
                    legendItem.numberFormat(item.numberFormat)
                }
                if (item.legendEntryWidth) {
                    legendItem.legendEntryWidth(item.legendEntryWidth as int)
                }
                if (item.legendEntryHeight) {
                    legendItem.legendEntryHeight(item.legendEntryHeight as int)
                }
                if (item.gapBetweenEntries) {
                    legendItem.gapBetweenEntries(item.gapBetweenEntries as int)
                }
                cartoBuilder.legend(legendItem)
            }
        }
        cartoBuilder
    }

    private Font getFont(Map fontItem) {
        String name = fontItem.name
        int style = Font.PLAIN
        if (fontItem.style.equalsIgnoreCase("bold")) {
            style = Font.BOLD
        } else if (fontItem.style.equalsIgnoreCase("italic")) {
            style = Font.ITALIC
        }
        int size = fontItem.size.toString().toInteger()
        new Font(name, style, size)
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/DBTilesTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import org.apache.commons.dbcp.BasicDataSource
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import javax.sql.DataSource

import static org.junit.jupiter.api.Assertions.*

import javax.imageio.ImageIO
import java.awt.Graphics2D
import java.awt.image.BufferedImage

/**
 * The DBTiles Unit Test
 * @author <NAME>
 */
class DBTilesTest {

    @TempDir
    private File folder

    private generate(DBTiles dbtiles) {
        TileRenderer renderer = new TileRenderer() {
            @Override
            byte[] render(Bounds b) {
                BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB)
                Graphics2D g2d = image.createGraphics()
                g2d.color = geoscript.filter.Color.randomPastel.asColor()
                g2d.fillRect(0, 0, 256, 256)
                g2d.dispose()
                ByteArrayOutputStream out = new ByteArrayOutputStream()
                ImageIO.write(image, "png", out)
                out.toByteArray()
            }
        }

        TileGenerator generator = new TileGenerator(verbose: false)
        generator.generate(dbtiles, renderer, 0, 2)
    }

    private void read(DBTiles dbtiles) {
        assertEquals("Random", dbtiles.name)
        Raster raster = dbtiles.getRaster(dbtiles.tiles(2))
        assertNotNull raster

        assertNotNull dbtiles.get(2,0,0)
        dbtiles.delete(new ImageTile(2,0,0))
        assertNull dbtiles.get(2,0,0).data
    }

    private metadata(DBTiles dbtiles) {
        Map<String,String> metadata = dbtiles.metadata
        assertEquals("Random", metadata.name)
        assertEquals("baselayer", metadata.type)
        assertEquals("1.0", metadata.version)
        assertEquals("Random Color Tiles", metadata.description)
        assertEquals("png", metadata.format)
        assertEquals("-179.99,-85.0511,179.99,85.0511", metadata.bounds)
        assertEquals("Created with GeoScript", metadata.attribution)

        List stats = dbtiles.tileCounts
        stats.eachWithIndex { Map stat, int index ->
            assertEquals(index, stat.zoom)
            assertEquals(Math.pow(4, index), stat.tiles, 0.1)
            assertEquals(Math.pow(4, index), stat.total, 0.1)
            assertEquals(1.0, stat.percent, 0.1)
        }

        assertEquals 0, dbtiles.minZoom
        assertEquals 2, dbtiles.maxZoom
    }

    @Test void h2() {
        File dbFile = new File(folder, "tiles.db")
        DBTiles dbtiles = new DBTiles("jdbc:h2:${dbFile}","org.h2.Driver", "Random", "Random Color Tiles")
        assertEquals("Random", dbtiles.name)
        generate(dbtiles)
        dbtiles = new DBTiles("jdbc:h2:${dbFile}","org.h2.Driver")
        assertEquals("Random", dbtiles.name)
        read(dbtiles)
    }

    @Test void h2DataSource() {
        File dbFile = new File(folder, "tiles.db")
        DataSource ds = new BasicDataSource()
        ds.url ="jdbc:h2:${dbFile}"
        ds.driverClassName = "org.h2.Driver"
        DBTiles dbtiles = new DBTiles(ds, "Random", "Random Color Tiles")
        assertEquals("Random", dbtiles.name)
        generate(dbtiles)
        dbtiles = new DBTiles(ds)
        assertEquals("Random", dbtiles.name)
        read(dbtiles)
    }

    @Test void h2Metadata() {
        File dbFile = new File(folder, "tiles.db")
        DBTiles dbtiles = new DBTiles("jdbc:h2:${dbFile}","org.h2.Driver", "Random", "Random Color Tiles")
        assertEquals("Random", dbtiles.name)
        generate(dbtiles)
        dbtiles = new DBTiles("jdbc:h2:${dbFile}","org.h2.Driver")
        assertEquals("Random", dbtiles.name)
        metadata(dbtiles)
    }

    @Test void sqlite() {
        File dbFile = new File("tiles.db") //new File(folder, "tiles.db")
        DBTiles dbtiles = new DBTiles("jdbc:sqlite:${dbFile}","org.sqlite.JDBC", "Random", "Random Color Tiles")
        assertEquals("Random", dbtiles.name)
        generate(dbtiles)
        dbtiles = new DBTiles("jdbc:sqlite:${dbFile}","org.sqlite.JDBC")
        assertEquals("Random", dbtiles.name)
        read(dbtiles)
    }

    @Test void sqliteDataSource() {
        File dbFile = new File(folder, "tiles.db")
        DataSource ds = new BasicDataSource()
        ds.url ="jdbc:sqlite:${dbFile}"
        ds.driverClassName = "org.sqlite.JDBC"
        DBTiles dbtiles = new DBTiles(ds, "Random", "Random Color Tiles")
        assertEquals("Random", dbtiles.name)
        generate(dbtiles)
        dbtiles = new DBTiles(ds)
        assertEquals("Random", dbtiles.name)
        read(dbtiles)
    }

    @Test void sqliteMetadata() {
        File dbFile = new File(folder, "tiles.db")
        DBTiles dbtiles = new DBTiles("jdbc:sqlite:${dbFile}","org.sqlite.JDBC", "Random", "Random Color Tiles")
        assertEquals("Random", dbtiles.name)
        generate(dbtiles)
        dbtiles = new DBTiles("jdbc:sqlite:${dbFile}","org.sqlite.JDBC")
        assertEquals("Random", dbtiles.name)
        metadata(dbtiles)
    }

    @Test void lookupWithTileFactories() {
        assertNotNull TileLayerFactories.list().find {
            it instanceof DBTiles.Factory
        }
    }

    @Test void createWithTileFactory() {
        File dbFile = new File(folder, "tiles.db")
        TileLayer dbtiles = TileLayer.getTileLayer("type=dbtiles driver='org.sqlite.JDBC' url='jdbc:sqlite:${dbFile}' name=Random description='Random Color Tiles'")
        generate(dbtiles as DBTiles)
    }

    @Test void getTileRendererWithTileFactory() {
        File dbFile = new File(folder, "tiles.db")
        DBTiles dbtiles = TileLayer.getTileLayer("type=dbtiles driver='org.sqlite.JDBC' url='jdbc:sqlite:${dbFile}' name=States description='The USA'") as DBTiles

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        TileRenderer renderer = TileLayer.getTileRenderer(dbtiles, new Shapefile(file))
        assertNotNull renderer

        TileGenerator generator = new TileGenerator(verbose: false)
        generator.generate(dbtiles, renderer, 0, 2)

        Raster raster = dbtiles.getRaster(dbtiles.tiles(2))
        assertNotNull raster

        assertNotNull dbtiles.get(2,0,0)
        dbtiles.delete(new ImageTile(2,0,0))
        assertNull dbtiles.get(2,0,0).data
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/io/KmlReaderTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.proj.Projection
import geoscript.workspace.Memory
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.layer.Layer

/**
 * The KmlReader UnitTest
 * @author <NAME>
 */
class KmlReaderTest {

    @TempDir
    File folder

    @Test void read() {
        String kml = """<kml:kml xmlns:kml="http://earth.google.com/kml/2.1">
    <kml:Document id="featureCollection">
        <kml:Placemark id="fid--259df7e1_131b6de0b8f_-8000">
            <kml:name>House</kml:name>
            <kml:Point>
                <kml:coordinates>111.0,-47.0</kml:coordinates>
            </kml:Point>
        </kml:Placemark>
        <kml:Placemark id="fid--259df7e1_131b6de0b8f_-7fff">
            <kml:name>School</kml:name>
            <kml:Point>
                <kml:coordinates>121.0,-45.0</kml:coordinates>
            </kml:Point>
        </kml:Placemark>
    </kml:Document>
</kml:kml>"""

        KmlReader reader = new KmlReader()

        // Read from a String
        Layer layer = reader.read(kml)
        assertNotNull layer
        assertEquals "kml", layer.name
        assertEquals "EPSG:4326", layer.proj.id
        assertTrue layer.schema.has("Geometry")
        assertTrue layer.schema.has("name")
        assertTrue layer.schema.has("description")
        assertEquals 2, layer.count
        layer.eachFeature{f ->
            assertNotNull f.geom
            assertNotNull f["name"]
            assertNull f["description"]
        }

        // Read from a String with custom name, projections, workspace
        layer = reader.read(kml, name: "points", projection: new Projection("EPSG:4326"), workspace: new Memory())
        assertNotNull layer
        assertEquals "points", layer.name
        assertEquals "EPSG:4326", layer.proj.id
        assertTrue layer.schema.has("Geometry")
        assertTrue layer.schema.has("name")
        assertTrue layer.schema.has("description")
        assertEquals 2, layer.count
        layer.eachFeature{f ->
            assertNotNull f.geom
            assertNotNull f["name"]
            assertNull f["description"]
        }

        // Read from an InputStream
        ByteArrayInputStream input = new ByteArrayInputStream(kml.getBytes("UTF-8"))
        layer = reader.read(input)
        assertNotNull layer
        assertEquals "kml", layer.name
        assertEquals "EPSG:4326", layer.proj.id
        assertTrue layer.schema.has("Geometry")
        assertTrue layer.schema.has("name")
        assertTrue layer.schema.has("description")
        assertEquals 2, layer.count
        layer.eachFeature{f ->
            assertNotNull f.geom
            assertNotNull f["name"]
            assertNull f["description"]
        }

        // Read from a File
        File file = new File(folder,"layer.kml")
        file.write(kml)
        layer = reader.read(file)
        assertNotNull layer
        assertEquals "kml", layer.name
        assertEquals "EPSG:4326", layer.proj.id
        assertTrue layer.schema.has("Geometry")
        assertTrue layer.schema.has("name")
        assertTrue layer.schema.has("description")
        assertEquals 2, layer.count
        layer.eachFeature{f ->
            assertNotNull f.geom
            assertNotNull f["name"]
            assertNull f["description"]
        }
    }
}



<|start_filename|>src/test/groovy/geoscript/style/HatchTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.filter.Expression
import geoscript.filter.Color

/**
 * The Hatch Unit Test
 * @author <NAME>
 */
class HatchTest {

    @Test void constructors() {

        Hatch hatch = new Hatch("horline")
        assertEquals "shape://horline", hatch.name.value
        assertEquals "#000000", hatch.stroke.color.value
        assertEquals 1.0, hatch.stroke.width.value, 0.1
        assertEquals 8, hatch.size.value, 0.1
        assertEquals "Hatch(name = shape://horline, fill = null, stroke = Stroke(color = #000000, width = 1), size = 8)", hatch.toString()

        def graphic = hatch.createHatch()
        assertEquals "shape://horline", graphic.graphicalSymbols()[0].wellKnownName.value
        assertEquals "#000000", graphic.graphicalSymbols()[0].stroke.color.value
        assertEquals 1.0, graphic.graphicalSymbols()[0].stroke.width.value, 0.1
        assertEquals 8, graphic.size.value, 0.1

        hatch = new Hatch("times", new Stroke("wheat", 1.2, [5,2], "square", "bevel"), 12.2)
        assertEquals "shape://times", hatch.name.value
        assertEquals "#f5deb3", hatch.stroke.color.value
        assertEquals 1.2, hatch.stroke.width.value, 0.1
        assertEquals 5, hatch.stroke.dash[0]
        assertEquals 2, hatch.stroke.dash[1]
        assertEquals "square", hatch.stroke.cap.value
        assertEquals "bevel", hatch.stroke.join.value
        assertEquals 12.2, hatch.size.value, 0.1
        assertEquals "Hatch(name = shape://times, fill = null, stroke = Stroke(color = #f5deb3, width = 1.2), size = 12.2)", hatch.toString()

        graphic = hatch.createHatch()
        assertEquals "shape://times", graphic.graphicalSymbols()[0].wellKnownName.value
        assertEquals "#f5deb3", graphic.graphicalSymbols()[0].stroke.color.value
        assertEquals 1.2, graphic.graphicalSymbols()[0].stroke.width.value, 0.1
        assertEquals 5, graphic.graphicalSymbols()[0].stroke.dashArray()[0].value, 0.1
        assertEquals 2, graphic.graphicalSymbols()[0].stroke.dashArray()[1].value, 0.1
        assertEquals "square", graphic.graphicalSymbols()[0].stroke.lineCap.value
        assertEquals "bevel", graphic.graphicalSymbols()[0].stroke.lineJoin.value
        assertEquals "bevel", graphic.graphicalSymbols()[0].stroke.lineJoin.value
        assertEquals 12.2, graphic.size.value, 0.1

        // Create a Hatch with Expressions
        hatch = new Hatch(new Expression("times"), new Stroke(new Color("wheat"), new Expression(1.2), [5,2], new Expression("square"), new Expression("bevel")), new Expression(12.2))
        assertEquals "shape://times", hatch.name.value
        assertEquals "#f5deb3", hatch.stroke.color.value
        assertEquals 1.2, hatch.stroke.width.value, 0.1
        assertEquals 5, hatch.stroke.dash[0]
        assertEquals 2, hatch.stroke.dash[1]
        assertEquals "square", hatch.stroke.cap.value
        assertEquals "bevel", hatch.stroke.join.value
        assertEquals 12.2, hatch.size.value, 0.1
        assertEquals "Hatch(name = shape://times, fill = null, stroke = Stroke(color = #f5deb3, width = 1.2), size = 12.2)", hatch.toString()

        graphic = hatch.createHatch()
        assertEquals "shape://times", graphic.graphicalSymbols()[0].wellKnownName.value
        assertEquals "#f5deb3", graphic.graphicalSymbols()[0].stroke.color.value
        assertEquals 1.2, graphic.graphicalSymbols()[0].stroke.width.value, 0.1
        assertEquals 5, graphic.graphicalSymbols()[0].stroke.dashArray().get(0).value, 0.1
        assertEquals 2, graphic.graphicalSymbols()[0].stroke.dashArray().get(1).value, 0.1
        assertEquals "square", graphic.graphicalSymbols()[0].stroke.lineCap.value
        assertEquals "bevel", graphic.graphicalSymbols()[0].stroke.lineJoin.value
        assertEquals "bevel", graphic.graphicalSymbols()[0].stroke.lineJoin.value
        assertEquals 12.2, graphic.size.value, 0.1

        // Named parameters
        hatch = new Hatch(size: 12, name: "slash", stroke: new Stroke("red",1.2))
        assertEquals "shape://slash", hatch.name.value
        assertEquals "#ff0000", hatch.stroke.color.value
        assertEquals 1.2, hatch.stroke.width.value, 0.1
        assertEquals 12, hatch.size.value, 0.1
        assertEquals "Hatch(name = shape://slash, fill = null, stroke = Stroke(color = #ff0000, width = 1.2), size = 12)", hatch.toString()

        // Hatch with Fill but no stroke
        hatch = new Hatch("circle", new Fill("red"), 8)
        assertEquals "circle", hatch.name.value
        assertEquals "#ff0000", hatch.fill.color.value
        assertNull hatch.stroke
        assertEquals 8, hatch.size.value, 0.1
        assertEquals "Hatch(name = circle, fill = Fill(color = #ff0000, opacity = 1.0), stroke = null, size = 8)", hatch.toString()

        // Hatch with Fill and stroke
        hatch = new Hatch("circle", new Fill("red"), new Stroke("wheat",0.1), 10)
        assertEquals "circle", hatch.name.value
        assertEquals "#ff0000", hatch.fill.color.value
        assertEquals "#f5deb3", hatch.stroke.color.value
        assertEquals 0.1, hatch.stroke.width.value, 0.1
        assertEquals 10, hatch.size.value, 0.1
        assertEquals "Hatch(name = circle, fill = Fill(color = #ff0000, opacity = 1.0), stroke = Stroke(color = #f5deb3, width = 0.1), size = 10)", hatch.toString()
    }
}


<|start_filename|>src/test/groovy/geoscript/geom/MultiPolygonTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue

class MultiPolygonTest {

    @Test void constructors() {
        MultiPolygon p1 = new MultiPolygon(new Polygon([1,2],[3,4],[5,6],[1,2]), new Polygon([7,8],[9,10],[11,12],[7,8]))
        assertEquals "MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8)))", p1.wkt

        MultiPolygon p2 = new MultiPolygon([[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]])
        assertEquals "MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8)))", p2.wkt

        MultiPolygon p3 = new MultiPolygon([new Polygon([1,2],[3,4],[5,6],[1,2]), new Polygon([7,8],[9,10],[11,12],[7,8])])
        assertEquals "MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8)))", p3.wkt

        MultiPolygon p4 = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        assertEquals "MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8)))", p4.wkt
    }

    @Test void plus() {
        def mp1 = new MultiPolygon(new Polygon([1,2],[3,4],[5,6],[1,2]), new Polygon([7,8],[9,10],[11,12],[7,8]))
        def mp2 = mp1 + new Polygon([11,12],[13,14],[15,16],[11,12])
        assertEquals "MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8)), ((11 12, 13 14, 15 16, 11 12)))", mp2.wkt
        def mp3 = new MultiPolygon([])
        assertEquals "MULTIPOLYGON EMPTY", mp3.wkt
        def mp4 = mp3 + new Polygon([1,2],[3,4],[4,5],[1,2])
        assertEquals "MULTIPOLYGON (((1 2, 3 4, 4 5, 1 2)))", mp4.wkt
    }

    @Test void split() {
        Geometry g = new Bounds(0,0,10,10).geometry
        Geometry split1 = g.split(new LineString([[0,0],[10,10]]))
        assertTrue split1 instanceof MultiPolygon
        assertEquals 2, split1.numGeometries
        Geometry split2 = split1.split(new LineString([[0,10], [10,0]]))
        assertTrue split2 instanceof MultiPolygon
        assertEquals 4, split2.numGeometries
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/Java2DCartoBuilder.groovy<|end_filename|>
package geoscript.carto

import geoscript.feature.Schema
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.geom.LineString
import geoscript.layer.Layer
import geoscript.proj.Projection
import geoscript.render.Map
import geoscript.style.ColorMap
import geoscript.workspace.Memory

import javax.imageio.ImageIO
import java.awt.BasicStroke
import java.awt.Color
import java.awt.Font
import java.awt.FontMetrics
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.font.LineBreakMeasurer
import java.awt.font.TextAttribute
import java.awt.geom.GeneralPath
import java.text.AttributedString
import java.text.DecimalFormat
import java.text.NumberFormat
import java.text.SimpleDateFormat

/**
 * A CartoBuilder that draws Items using Java2D.
 * @author <NAME>
 */
class Java2DCartoBuilder implements CartoBuilder {

    protected final Graphics2D graphics

    protected final PageSize pageSize

    /**
     * Create a new Java2DCartoBuilder with a Graphics2D context and PageSize
     * @param graphics The Graphics2D context
     * @param pageSize The PageSize
     */
    Java2DCartoBuilder(Graphics2D graphics, PageSize pageSize) {
        this.graphics = graphics
        this.pageSize = pageSize
    }

    @Override
    CartoBuilder dateText(DateTextItem dateTextItem) {
        if (dateTextItem.date == null) dateTextItem.date = new Date()
        graphics.color = dateTextItem.color
        graphics.font = dateTextItem.font
        def formatter = new SimpleDateFormat(dateTextItem.format)
        drawString(formatter.format(dateTextItem.date), dateTextItem.getRectangle(), dateTextItem.horizontalAlign, dateTextItem.verticalAlign)
        this
    }

    @Override
    CartoBuilder image(ImageItem imageItem) {
        def image = ImageIO.read(imageItem.path)
        graphics.drawImage(image, imageItem.x, imageItem.y, imageItem.width, imageItem.height, null)
        this
    }

    @Override
    CartoBuilder line(LineItem lineItem) {
        graphics.color = lineItem.strokeColor
        graphics.stroke = new BasicStroke(lineItem.strokeWidth)
        graphics.drawLine(lineItem.x,lineItem.y,lineItem.x+lineItem.width,lineItem.y+lineItem.height)
        this
    }

    @Override
    CartoBuilder map(MapItem mapItem) {
        mapItem.map.width = mapItem.width
        mapItem.map.height = mapItem.height
        graphics.drawImage(mapItem.map.renderToImage(), mapItem.x, mapItem.y, null)
        this
    }

    @Override
    CartoBuilder overViewMap(OverviewMapItem overviewMapItem) {
        overviewMapItem.overviewMap.width = overviewMapItem.width
        overviewMapItem.overviewMap.height = overviewMapItem.height
        if (overviewMapItem.zoomIntoBounds) {
            overviewMapItem.overviewMap.bounds = overviewMapItem.linkedMap.bounds.scale(overviewMapItem.scaleFactor)
        }
        Projection overviewMapProjection = overviewMapItem.overviewMap.proj ?: new Projection("EPSG:4326")
        Projection linkedMapProjection = overviewMapItem.linkedMap.proj ?: new Projection("EPSG:4326")
        Layer areaLayer = new Memory().create(new Schema("area","geom:Polygon:srid=${overviewMapProjection.epsg}"))
        Geometry geometry = Projection.transform(overviewMapItem.linkedMap.bounds.geometry, linkedMapProjection, overviewMapProjection)
        areaLayer.add([geom: geometry])
        areaLayer.style = overviewMapItem.areaStyle
        overviewMapItem.overviewMap.addLayer(areaLayer)
        graphics.drawImage(overviewMapItem.overviewMap.renderToImage(), overviewMapItem.x, overviewMapItem.y, null)
        overviewMapItem.overviewMap.layers.remove(areaLayer)
        this
    }

    @Override
    CartoBuilder northArrow(NorthArrowItem northArrowItem) {
        if(northArrowItem.style == NorthArrowStyle.North) {
            drawNorthArrow(northArrowItem)
        } else if(northArrowItem.style == NorthArrowStyle.NorthEastSouthWest) {
            drawNESWArrow(northArrowItem)
        }
        this
    }

    private void drawNorthArrow(NorthArrowItem northArrowItem) {

        int x = northArrowItem.x
        int y = northArrowItem.y
        int width = northArrowItem.width
        int height = northArrowItem.height
        int strokeWidth = northArrowItem.strokeWidth
        Color fillColor1 = northArrowItem.fillColor1
        Color fillColor2 = northArrowItem.fillColor2
        Color strokeColor1 = northArrowItem.strokeColor1
        Color strokeColor2 = northArrowItem.strokeColor2

        if (northArrowItem.drawText) {
            graphics.color = northArrowItem.textColor
            graphics.font = northArrowItem.font
            FontMetrics fontMetrics = graphics.fontMetrics
            String text = "N"
            int textHeight = fontMetrics.height
            drawString(text, new Rectangle(x, y + (height - textHeight), width, textHeight), HorizontalAlign.CENTER, VerticalAlign.BOTTOM)
            height = height - textHeight
        }

        def path1 = new GeneralPath()
        path1.moveTo((x + width/2) as double, y)
        path1.lineTo(x, y + height)
        path1.lineTo((x + width / 2) as double, (y + height * 3/4) as double)
        path1.closePath()

        def path2 = new GeneralPath()
        path2.moveTo((x + width/2) as double, y)
        path2.lineTo(x + width, y + height)
        path2.lineTo((x + width/2) as double, (y + height * 3/4) as double)
        path2.closePath()

        graphics.stroke = new BasicStroke(strokeWidth)

        graphics.color = fillColor1
        graphics.fill(path1)
        graphics.color = strokeColor1
        graphics.draw(path1)

        graphics.color = fillColor2
        graphics.fill(path2)
        graphics.color = strokeColor2
        graphics.draw(path2)
    }

    private void drawNESWArrow(NorthArrowItem northArrowItem) {

        int x = northArrowItem.x
        int y = northArrowItem.y
        int width = northArrowItem.width
        int height = northArrowItem.height

        if (northArrowItem.drawText) {
            graphics.color = northArrowItem.textColor
            graphics.font = northArrowItem.font
            FontMetrics fontMetrics = graphics.fontMetrics
            int textWidth = [fontMetrics.stringWidth("N"), fontMetrics.stringWidth("E"), fontMetrics.stringWidth("S"), fontMetrics.stringWidth("W")].max()
            int textHeight = fontMetrics.height
            drawString("N", new Rectangle(x + (width / 2 - textWidth / 2) as int, y, textWidth, textHeight), HorizontalAlign.CENTER, VerticalAlign.TOP)
            drawString("E", new Rectangle(x + (width - textWidth), y + (height / 2 - textHeight / 2) as int, textWidth, textHeight), HorizontalAlign.RIGHT, VerticalAlign.MIDDLE)
            drawString("S", new Rectangle(x + (width / 2 - textWidth / 2) as int, y + (height - textHeight), textWidth, textHeight), HorizontalAlign.CENTER, VerticalAlign.TOP)
            drawString("W", new Rectangle(x, y + (height / 2 - textHeight / 2) as int, textWidth, textHeight), HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
            x = x + textWidth
            y = y + textHeight
            width = width - (textWidth * 2)
            height = height - (textHeight * 2)
        }

        Point north = new Point((x + (width / 2) as int) - 2, y)
        Point east  = new Point(x + width, y + (height / 2) as int)
        Point south = new Point((x + (width / 2) as int) - 2, y + height)
        Point west  = new Point(x, y + (height / 2) as int)

        Point mid = new Point(x + (width / 2) as int, y + (height / 2) as int)

        Point northEast = new Point(x + (width * 2/3) as int, y + (height * 1/3) as int)
        Point southEast  = new Point(x + (width * 2/3) as int, y + (height * 2/3) as int)
        Point southWest  = new Point(x + (width * 1/3) as int, y + (height * 2/3) as int)
        Point northWest  = new Point(x + (width * 1/3) as int, y + (height * 1/3) as int)

        // Fill
        graphics.color = northArrowItem.fillColor1
        graphics.fill(createPolygon(north, northEast, mid))
        graphics.color = northArrowItem.fillColor2
        graphics.fill(createPolygon(north, northWest, mid))

        graphics.color = northArrowItem.fillColor1
        graphics.fill(createPolygon(east, southEast, mid))
        graphics.color = northArrowItem.fillColor2
        graphics.fill(createPolygon(east, northEast, mid))

        graphics.color = northArrowItem.fillColor1
        graphics.fill(createPolygon(south, southWest, mid))
        graphics.color = northArrowItem.fillColor2
        graphics.fill(createPolygon(south, southEast, mid))

        graphics.color = northArrowItem.fillColor1
        graphics.fill(createPolygon(west, northWest, mid))
        graphics.color = northArrowItem.fillColor2
        graphics.fill(createPolygon(west, southWest, mid))

        // Stroke
        graphics.color = northArrowItem.strokeColor1
        graphics.draw(createPolygon(north, northEast, mid))
        graphics.color = northArrowItem.strokeColor2
        graphics.draw(createPolygon(north, northWest, mid))

        graphics.color = northArrowItem.strokeColor1
        graphics.draw(createPolygon(east, southEast, mid))
        graphics.color = northArrowItem.strokeColor2
        graphics.draw(createPolygon(east, northEast, mid))

        graphics.color = northArrowItem.strokeColor1
        graphics.draw(createPolygon(south, southWest, mid))
        graphics.color = northArrowItem.strokeColor2
        graphics.draw(createPolygon(south, southEast, mid))

        graphics.color = northArrowItem.strokeColor1
        graphics.draw(createPolygon(west, northWest, mid))
        graphics.color = northArrowItem.strokeColor2
        graphics.draw(createPolygon(west, southWest, mid))
    }

    private GeneralPath createPolygon(Point point1, Point point2, Point point3) {
        GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 3)
        path.moveTo(point1.x as int, point1.y as int)
        path.lineTo(point2.x as int, point2.y as int)
        path.lineTo(point3.x as int, point3.y as int)
        path.closePath()
        path
    }

    @Override
    CartoBuilder text(TextItem textItem) {
        graphics.color = textItem.color
        graphics.font = textItem.font
        drawString(textItem.text, textItem.getRectangle(), textItem.horizontalAlign, textItem.verticalAlign)
        this
    }

    @Override
    CartoBuilder paragraph(ParagraphItem paragraphItem) {

        int x = paragraphItem.x
        int y = paragraphItem.y
        int width = paragraphItem.width
        int height = paragraphItem.height
        String text = paragraphItem.text
        Font font = paragraphItem.font
        Color color = paragraphItem.color

        graphics.color = color
        graphics.font = font

        def attributedString = new AttributedString(text)
        attributedString.addAttribute(TextAttribute.FONT, font)
        def paragraph = attributedString.iterator
        int paragraphStart = paragraph.beginIndex
        int paragraphEnd = paragraph.endIndex
        def context = graphics.fontRenderContext
        def lineMeasurer = new LineBreakMeasurer(paragraph, context)
        float breakWidth = width as float
        float drawPosY = y as float
        lineMeasurer.position = paragraphStart

        while(lineMeasurer.position < paragraphEnd) {
            def layout = lineMeasurer.nextLayout(breakWidth)
            float drawPosX = layout.isLeftToRight() ? x : breakWidth - layout.advance
            drawPosY += layout.ascent
            layout.draw(graphics, drawPosX, drawPosY)
            drawPosY += layout.descent + layout.leading
        }
        this
    }

    @Override
    CartoBuilder rectangle(RectangleItem rectangleItem) {

        int x = rectangleItem.x
        int y = rectangleItem.y
        int width = rectangleItem.width
        int height = rectangleItem.height
        Color strokeColor = rectangleItem.strokeColor
        Color fillColor = rectangleItem.fillColor
        int strokeWidth = rectangleItem.strokeWidth

        if (fillColor) {
            graphics.color = fillColor
            graphics.fillRect(x, y, width, height)
        }
        graphics.color = strokeColor
        graphics.stroke = new BasicStroke(strokeWidth)
        graphics.drawRect(x,y,width,height)

        this
    }

    @Override
    CartoBuilder grid(GridItem gridItem) {
        graphics.color = gridItem.strokeColor
        graphics.stroke = new BasicStroke(gridItem.strokeWidth)
        int numberWide = pageSize.width / gridItem.size
        int numberHigh = pageSize.height / gridItem.size
        (0..numberWide).each { int c ->
            int x = gridItem.x + gridItem.size * c
            (0..numberHigh).each { int r ->
                int y = gridItem.y + gridItem.size * r
                graphics.drawRect(x,y, gridItem.size, gridItem.size)
            }
        }
        this
    }

    @Override
    CartoBuilder scaleText(ScaleTextItem scaleTextItem) {

        Font font = scaleTextItem.font
        Color color = scaleTextItem.color
        String format = scaleTextItem.format
        Map map = scaleTextItem.map

        graphics.color = color
        graphics.font = font
        def formatter = new DecimalFormat(format)
        drawString("${scaleTextItem.prefixText}1:${formatter.format(map.scaleDenominator)}", scaleTextItem.getRectangle(), scaleTextItem.horizontalAlign, scaleTextItem.verticalAlign)

        this
    }

    @Override
    CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {

        int x = scaleBarItem.x
        int y = scaleBarItem.y
        int width = scaleBarItem.width
        int height = scaleBarItem.height
        int border = 5
        Font font = scaleBarItem.font
        Color strokeColor = scaleBarItem.strokeColor
        Color fillColor = scaleBarItem.fillColor
        int strokeWidth = scaleBarItem.strokeWidth
        int ticHeight = 10

        ScaleBarItem.ScaleBarInfo scaleBarInfo = scaleBarItem.calculateScaleBarInfo()

        // Check if the scalebar width matches the item width
        int padding = 0
        if (scaleBarInfo.widthInPixels == scaleBarItem.width) {
            padding = 5
        }

        Rectangle r = new Rectangle(x,y,width,height)
        int lineXStart = (int)((r.x + r.width / 2) - scaleBarInfo.widthInPixels / 2) + padding
        int lineXEnd = (int)(lineXStart + scaleBarInfo.widthInPixels)
        int lineY = y + height - border

        if (fillColor) {
            graphics.color = fillColor
            graphics.fillRect(lineXStart - border, y, lineXEnd - lineXStart + border * 2, height)
        }
        graphics.color = strokeColor
        graphics.stroke = new BasicStroke(strokeWidth)
        graphics.drawRect(lineXStart - border, y, lineXEnd - lineXStart + border * 2, height)

        graphics.drawLine(lineXStart, lineY, lineXEnd, lineY)
        graphics.drawLine(lineXStart, y + height - border, lineXStart, y + height - border - ticHeight)
        graphics.drawLine(lineXEnd, y + height - border, lineXEnd, y + height - border - ticHeight)
        graphics.font = font
        String scaleText = "${(int) scaleBarInfo.widthInUnits} ${scaleBarInfo.unitForScaleText}"
        drawString(scaleText, new Rectangle(
            lineXStart,
            y + border,
            (int) scaleBarInfo.widthInPixels,
            height - border * 2
        ), HorizontalAlign.CENTER, VerticalAlign.MIDDLE)

        this
    }

    @Override
    CartoBuilder table(TableItem tableItem) {

        // Draw Headers
        (0..<tableItem.columns.size()).each { int c ->

            graphics.font = tableItem.columnRowStyle.font
            FontMetrics fontMetrics = graphics.fontMetrics
            int columnWidth = tableItem.width / tableItem.columns.size()
            int lastColumnWidth = columnWidth + (tableItem.width - (columnWidth * tableItem.columns.size() - 1)) - 1
            int rowHeight = fontMetrics.height + 4

            Rectangle rectangle = new Rectangle(
                tableItem.x + columnWidth * c,
                tableItem.y,
                c == tableItem.columns.size() - 1 ? lastColumnWidth : columnWidth,
                rowHeight
            )

            graphics.color = tableItem.columnRowStyle.backGroundColor
            graphics.fillRect(rectangle.x as int, rectangle.y as int, rectangle.width as int, rectangle.height as int)

            graphics.color = tableItem.columnRowStyle.strokeColor
            graphics.drawRect(rectangle.x as int, rectangle.y as int, rectangle.width as int, rectangle.height as int)

            graphics.color = tableItem.columnRowStyle.textColor
            drawString(" ${tableItem.columns[c]}", rectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
        }
        // Draw rows
        (0..<tableItem.rows.size()).each { int r ->

            boolean isEven = r % 2
            graphics.font = isEven ? tableItem.evenRowStyle.font : tableItem.oddRowStyle.font
            FontMetrics fontMetrics = graphics.fontMetrics
            int columnWidth = tableItem.width / tableItem.columns.size()
            int lastColumnWidth = columnWidth + (tableItem.width - (columnWidth * tableItem.columns.size() - 1)) - 1
            int rowHeight = fontMetrics.height + 4

            int rowY = (tableItem.y + rowHeight * r) + rowHeight
            java.util.Map values = tableItem.rows[r]

            (0..<tableItem.columns.size()).each { int c ->

                Rectangle rectangle = new Rectangle(
                    tableItem.x + columnWidth * c,
                    rowY,
                    c == tableItem.columns.size() - 1 ? lastColumnWidth : columnWidth,
                    rowHeight
                )

                graphics.color = isEven ? tableItem.evenRowStyle.backGroundColor : tableItem.oddRowStyle.backGroundColor
                graphics.fillRect(rectangle.x as int, rectangle.y as int, rectangle.width as int, rectangle.height as int)

                graphics.color = isEven ? tableItem.evenRowStyle.strokeColor : tableItem.oddRowStyle.strokeColor
                graphics.drawRect(rectangle.x as int, rectangle.y as int, rectangle.width as int, rectangle.height as int)

                graphics.color = isEven ? tableItem.evenRowStyle.textColor : tableItem.oddRowStyle.textColor
                drawString(" ${values[tableItem.columns[c]]}", rectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
            }
        }
        this
    }

    @Override
    CartoBuilder legend(LegendItem legendItem) {

        // Draw background
        if(legendItem.backgroundColor) {
            graphics.color = legendItem.backgroundColor
            graphics.fillRect(legendItem.x, legendItem.y, legendItem.width, legendItem.height)
        }

        // Draw title
        graphics.font = legendItem.titleFont
        graphics.color = legendItem.titleColor
        FontMetrics fm = graphics.fontMetrics
        int titleHeight = fm.height
        drawString(legendItem.title, new Rectangle(legendItem.x, legendItem.y, legendItem.width, titleHeight), HorizontalAlign.LEFT, VerticalAlign.MIDDLE)

        // Draw Entries
        graphics.font = legendItem.textFont
        graphics.color = legendItem.textColor
        fm = graphics.fontMetrics

        // Keep track of the entry x and y
        int entryX = legendItem.x
        int entryY = legendItem.y + titleHeight + legendItem.gapBetweenEntries
        int maxTextWidth = -1

        legendItem.entries.eachWithIndex { LegendItem.LegendEntry entry, int i ->

            if (entry.type == LegendItem.LegendEntryType.COLORMAP) {

                // Draw title
                graphics.color = legendItem.textColor
                Rectangle titleRectangle = new Rectangle(entryX, entryY, legendItem.legendEntryWidth * 2, fm.height)
                drawString(entry.title, titleRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
                entryY += fm.height + legendItem.gapBetweenEntries

                NumberFormat numberFormat = new DecimalFormat(legendItem.numberFormat)
                ColorMap colorMap = entry.symbolizer as ColorMap
                colorMap.values.each { java.util.Map value ->

                    String title = numberFormat.format(value.quantity)
                    String colorHex = "${value.color}"

                    Rectangle symbolRectangle = new Rectangle(
                            entryX,
                            entryY,
                            legendItem.legendEntryWidth,
                            legendItem.legendEntryHeight
                    )
                    graphics.color = Color.decode(colorHex)
                    graphics.fill(symbolRectangle)

                    Rectangle textRectangle = new Rectangle(
                            entryX + legendItem.legendEntryWidth + legendItem.gapBetweenEntries,
                            entryY,
                            legendItem.legendEntryWidth,
                            legendItem.legendEntryHeight
                    )
                    maxTextWidth = Math.max(maxTextWidth, fm.stringWidth(title) + legendItem.gapBetweenEntries)
                    graphics.color = legendItem.textColor
                    drawString(title, textRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)

                    entryY += legendItem.legendEntryHeight
                }

                entryY += legendItem.gapBetweenEntries

            } else if (entry.type == LegendItem.LegendEntryType.IMAGE) {

                // Draw Image
                Rectangle imageRectangle = new Rectangle(entryX, entryY, legendItem.legendEntryWidth, legendItem.legendEntryHeight)
                graphics.drawImage(entry.image, imageRectangle.x as int, imageRectangle.y  as int, imageRectangle.width  as int, imageRectangle.height  as int, null)

                // Draw title
                graphics.color = legendItem.textColor
                Rectangle titleRectangle = new Rectangle(
                    entryX + legendItem.legendEntryWidth + legendItem.gapBetweenEntries,
                    entryY,
                    legendItem.legendEntryWidth,
                    legendItem.legendEntryHeight
                )
                drawString(entry.title, titleRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)

                entryY += legendItem.legendEntryHeight + legendItem.gapBetweenEntries

            } else if (entry.type == LegendItem.LegendEntryType.GROUP) {

                // Draw title
                graphics.color = legendItem.textColor
                Rectangle titleRectangle = new Rectangle(entryX, entryY, legendItem.legendEntryWidth * 2, fm.height)
                drawString(entry.title, titleRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)
                entryY += fm.height + legendItem.gapBetweenEntries

            } else /* POINT, LINE, POLYGON */ {

                Rectangle symbolRectangle = new Rectangle(
                        entryX,
                        entryY,
                        legendItem.legendEntryWidth,
                        legendItem.legendEntryHeight
                )

                Geometry geometry
                if (entry.type == LegendItem.LegendEntryType.POLYGON) {
                    geometry = new Bounds(2, 2, legendItem.legendEntryWidth - 2, legendItem.legendEntryHeight - 2).geometry
                } else if (entry.type == LegendItem.LegendEntryType.LINE) {
                    geometry = new LineString([[0, legendItem.legendEntryHeight / 2], [legendItem.legendEntryWidth, legendItem.legendEntryHeight / 2]])
                } else if (entry.type == LegendItem.LegendEntryType.POINT) {
                    geometry = new geoscript.geom.Point((legendItem.legendEntryWidth / 2) as int, (legendItem.legendEntryHeight / 2) as int)
                }

                Layer layer = Layer.fromGeometry("polygon", geometry, style: entry.symbolizer)
                Map map = new Map(
                        width: legendItem.legendEntryWidth,
                        height: legendItem.legendEntryHeight,
                        fixAspectRatio: false,
                        bounds: new Bounds(0, 0, legendItem.legendEntryWidth, legendItem.legendEntryHeight),
                        layers: [layer]
                )
                graphics.drawImage(map.renderToImage(), symbolRectangle.x as int, symbolRectangle.y as int, null)

                Rectangle textRectangle = new Rectangle(
                        entryX + legendItem.legendEntryWidth + legendItem.gapBetweenEntries,
                        entryY,
                        legendItem.legendEntryWidth,
                        legendItem.legendEntryHeight
                )
                maxTextWidth = Math.max(maxTextWidth, fm.stringWidth(entry.title) + legendItem.gapBetweenEntries)
                drawString(entry.title, textRectangle, HorizontalAlign.LEFT, VerticalAlign.MIDDLE)

                entryY += legendItem.legendEntryHeight + legendItem.gapBetweenEntries
            }

            // Are there more entries?
            if (i < (legendItem.entries.size() - 1)) {
                // Check to make sure the next entry can fit in the remaining space
                int nextHeight = getLegendItemHeight(legendItem, legendItem.entries[i + 1])
                if ((entryY + nextHeight) > legendItem.height) {
                    // Move the entry x over to the right to a separate column
                    entryX += legendItem.legendEntryWidth + legendItem.gapBetweenEntries + maxTextWidth
                    // Reset the entry y to the top
                    entryY = legendItem.y + titleHeight + legendItem.gapBetweenEntries
                    // Reset the max text width
                    maxTextWidth = -1
                }
            }
        }
        this
    }

    private int getLegendItemHeight(LegendItem item, LegendItem.LegendEntry entry) {
        int height
        if (entry.type == LegendItem.LegendEntryType.COLORMAP) {
            graphics.font = item.textFont
            height = graphics.fontMetrics.height + item.gapBetweenEntries + (entry.symbolizer.values.size() * item.legendEntryHeight) + item.gapBetweenEntries
        } else if (entry.type == LegendItem.LegendEntryType.GROUP) {
            graphics.font = item.textFont
            height = graphics.fontMetrics.height + item.gapBetweenEntries
        } else {
            height = item.legendEntryHeight + item.gapBetweenEntries
        }
        height
    }

    private void drawString(String text, Rectangle rectangle, HorizontalAlign horizontalAlign, VerticalAlign verticalAlign) {
      drawString(text, rectangle, horizontalAlign, verticalAlign, false)
    }

    private void drawString(String text, Rectangle rectangle, HorizontalAlign horizontalAlign, VerticalAlign verticalAlign, boolean debug) {
        FontMetrics fontMetrics = graphics.getFontMetrics(graphics.font)
        int x
        if (horizontalAlign == HorizontalAlign.LEFT) {
            x = rectangle.x
        } else if (horizontalAlign == HorizontalAlign.CENTER) {
            x = rectangle.x + (rectangle.width - fontMetrics.stringWidth(text)) / 2
        } else if (horizontalAlign == HorizontalAlign.RIGHT) {
            x = rectangle.x +  rectangle.width - fontMetrics.stringWidth(text)
        }
        int y
        if (verticalAlign == VerticalAlign.TOP) {
            y = rectangle.y + fontMetrics.height - fontMetrics.descent
        } else if (verticalAlign == VerticalAlign.MIDDLE) {
            y = rectangle.y + ((rectangle.height - fontMetrics.height) / 2) + fontMetrics.height - fontMetrics.descent
        } else if (verticalAlign == VerticalAlign.BOTTOM) {
            y = rectangle.y + rectangle.height - fontMetrics.descent
        }
        if (debug) {
            graphics.drawRect(rectangle.x as int, rectangle.y as int, rectangle.width as int, rectangle.height as int)
            graphics.drawRect(x, y - fontMetrics.height + fontMetrics.descent, fontMetrics.stringWidth(text), fontMetrics.height)
        }
        graphics.drawString(text, x, y)
    }

    @Override
    void build(OutputStream outputStream) {
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/CartoFactory.groovy<|end_filename|>
package geoscript.carto

/**
 * A CartoFactory can create a CartoBuilder with a given PageSize.
 * @author <NAME>
 */
interface CartoFactory {

    /**
     * Create a CartoBuilder with the given PageSize
     * @param pageSize The PageSize
     * @return A CartoBuilder
     */
    CartoBuilder create(PageSize pageSize)

    /**
     * The CartoFactory name
     * @return The name of the CartoFactory
     */
    String getName()

    /**
     * The CartoFactory mime type
     * @return The mime type of the CartoFactory
     */
    String getMimeType()

}


<|start_filename|>src/main/groovy/geoscript/feature/io/KmlWriter.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Feature
import geoscript.filter.Expression
import org.geotools.kml.KML
import org.geotools.kml.KMLConfiguration
import org.geotools.xsd.Configuration
import org.geotools.xsd.Encoder

/**
 * Write a Feature as a KML Placemark.
 * @author <NAME>
 */
class KmlWriter implements Writer {

    /**
     * Write a Feature as a KML Placemark
     * @param options The named parameters
     * <ul>
     *     <li>format = Whether to format the KML or not (default = false)</li>
     *     <li>xmldecl = Whether to include the XML declaration (default = false)</li>
     * </ul>
     * @param feature The Feature
     * @return A KML Placemark String
     */
    String write(Map options = [:], Feature feature) {
        boolean format = options.get("format", false)
        boolean xmldecl = options.get("xmldecl", false)
        Configuration config = new KMLConfiguration()
        Encoder encoder = new Encoder(config)
        encoder.indenting = format
        encoder.omitXMLDeclaration = !xmldecl
        encoder.encodeAsString(feature.f, KML.Placemark)
    }

    /**
     * Write a Feature to KML using a Groovy Markup Builder
     * @param options The named parameters
     * <ul>
     *      <li>namespace = The KML namespace prefix (defaults to blank)</li>
     *      <li>name = The name value can be a string value, an Expression, or a Closure</li>
     *      <li>description = The description value can be a string value, an Expression or a Closure</li>
     *      <li>extendedData = Whether to include all attributes as ExtentData</li>
     *      <li>includeStyle = Whether to include a simple style element or not</li>
     *      <li>color = The color of the style element</li>
     * </ul>
     * @param builder The Groovy MarkupBuilder
     * @param feature The Feature
     */
    void write(Map options = [:], def builder, Feature feature) {
        String namespace = options.get("namespace","")
        String ns = namespace.isEmpty() ? "" : "${namespace}:"
        def nameValue = options.get("name", {Feature f -> f.id})
        def descriptionValue = options.get("description")
        boolean extendedData = options.get("extendedData", true)
        boolean includeStyle = options.get("includeStyle", true)
        String color = geoscript.filter.Color.toHex(options.get("color","#ff0000ff")).replace("#","")
        String geometryType = feature.schema.geom.typ.toLowerCase()
        def geometryWriter = new geoscript.geom.io.KmlWriter()
        builder."${ns}Placemark" {
            builder."${ns}name" { mkp.yield(getValue(nameValue, feature)) }
            if (descriptionValue != null) {
                builder."${ns}description" { mkp.yield(getValue(descriptionValue, feature)) }
            }
            if (includeStyle) {
                builder."${ns}Style" {
                    if (geometryType.endsWith("point")) {
                        builder."${ns}IconStyle" {
                            builder."${ns}color"("${color}")
                        }
                    } else {
                        builder."${ns}LineStyle" {
                            builder."${ns}color"("${color}")
                        }
                        if (geometryType.endsWith("Polygon")) {
                            builder."${ns}PolygonStyle" {
                                builder."${ns}Fill"("0")
                            }
                        }
                    }
                }
            }
            if (extendedData) {
                builder."${ns}ExtendedData" {
                    builder."${ns}SchemaData" ("${ns}schemaUrl": "#${feature.schema.name}") {
                        feature.schema.fields.each {fld ->
                            if (!fld.isGeometry()) {
                                builder."${ns}SimpleData" ("${ns}name": fld.name) { mkp.yield(feature.get(fld.name)) }
                            }
                        }
                    }
                }
            }
            geometryWriter.write builder, feature.geom, namespace: namespace
        }
    }

    /**
     * Get a String value from a Closure, Expression or value
     * @param template The Closure, Expression, or value
     * @param obj The context Object (Layer, Feature)
     * @return A value
     */
    private String getValue(Object template, Object obj) {
        if (template instanceof Closure) {
            (template as Closure).call(obj)
        } else if (template instanceof Expression) {
            (template as Expression).evaluate(obj)
        } else {
            template as String
        }
    }
}

<|start_filename|>src/test/groovy/geoscript/workspace/FlatGeobufTest.groovy<|end_filename|>
package geoscript.workspace

import geoscript.FileUtil
import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.geom.LineString
import geoscript.geom.Point
import geoscript.layer.Layer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The FlatGeobuf Workspace Unit Test
 * @author <NAME>
 */
class FlatGeobufTest {

    @TempDir
    File folder

    @Test void add() {

        // Create a new FlatGeobuf Workspace
        File directory = FileUtil.createDir(folder,"flatgeobufs")
        FlatGeobuf geobuf = new FlatGeobuf(directory)

        // Create an in memory Layer
        Memory memory = new Memory()
        Layer memoryLayer = memory.create('locations',[new Field("geom", "Point"), new Field("name", "String")])
        assertNotNull(memoryLayer)
        memoryLayer.add([new Point(1,1), "Seattle"])
        memoryLayer.add([new Point(2,2), "Portland"])
        memoryLayer.add([new Point(3,3), "Tacoma"])
        assertEquals 3, memoryLayer.count()

        // And add it to FlatGeobuf
        assertNotNull geobuf.add(memoryLayer)
        assertTrue new File(directory, "locations.fgb").exists()

        // Make sure we can read it from FlatGeobuf
        Layer geobufLayer = geobuf.get("locations")
        assertEquals 3, memoryLayer.count
    }

    @Test void remove() {
        File directory = FileUtil.createDir(folder,"flatgeobufs")
        Workspace workspace = new FlatGeobuf(directory)
        workspace.create("points",[new Field("geom","Point","EPSG:4326")])
        workspace.get("points").add(new Feature([geom: new Point(1,1)],"id.1"))
        workspace.create("lines",[new Field("geom","LineString","EPSG:4326")])
        workspace.get("lines").add(new Feature([geom: new LineString([[1,1],[10,10]])],"id.1"))
        workspace.create("polygons",[new Field("geom","Polygon","EPSG:4326")])
        workspace.get("polygons").add(new Feature([geom: new Point(1,1).buffer(5)],"id.1"))
        assertTrue workspace.has("points")
        assertTrue workspace.has("lines")
        assertTrue workspace.has("polygons")
        workspace.remove("points")
        workspace.remove(workspace.get("lines"))
        workspace.remove(workspace.get("polygons"))
        // @TODO Removed layers are not being remove from typeNames
        workspace = new FlatGeobuf(directory)
        assertFalse workspace.has("points")
        assertFalse workspace.has("lines")
        assertFalse workspace.has("polygons")
    }

    @Test void getWorkspace() {
        File directory = FileUtil.createDir(folder,"flatgeobufs")
        Workspace workspace = new FlatGeobuf(directory)
        workspace.create("points",[new Field("geom","Point","EPSG:4326")])
        workspace.get("points").add(new Feature([geom: new Point(1,1)],"id.1"))

        Workspace w = Workspace.getWorkspace(new File(directory, "points.fgb").absolutePath)
        assertNotNull w
        assertEquals("FlatGeobuf", w.format)
    }

    @Test void getWorkspaceFromString() {
        File directory = FileUtil.createDir(folder,"flatgeobufs")
        Workspace workspace = new FlatGeobuf(directory)
        workspace.create("points",[new Field("geom","Point","EPSG:4326")])
        workspace.get("points").add(new Feature([geom: new Point(1,1)],"id.1"))
        workspace.close()
        FlatGeobuf geobuf = Workspace.getWorkspace("type=flatgeobuf file=${directory}")
        assertNotNull geobuf
        assertTrue geobuf.names.contains("points")
        geobuf = Workspace.getWorkspace("type=flatgeobuf file=${new File(directory, 'points.fgb')}")
        assertNotNull geobuf
        assertTrue geobuf.names.contains("points")
    }

    @Test void getWorkspaceFromMap() {
        File directory = FileUtil.createDir(folder,"flatgeobufs")
        Workspace workspace = new FlatGeobuf(directory)
        workspace.create("points",[new Field("geom","Point","EPSG:4326")])
        workspace.get("points").add(new Feature([geom: new Point(1,1)],"id.1"))
        workspace.close()
        FlatGeobuf geobuf = Workspace.getWorkspace([type: 'flatgeobuf', file: directory])
        assertNotNull geobuf
        assertTrue geobuf.names.contains("points")
        geobuf = Workspace.getWorkspace([type: 'flatgeobuf', file: new File(directory, 'points.fgb')])
        assertNotNull geobuf
        assertTrue geobuf.names.contains("points")
        geobuf = Workspace.getWorkspace([type: 'flatgeobuf', file: directory.absolutePath])
        assertNotNull geobuf
        assertTrue geobuf.names.contains("points")
        geobuf = Workspace.getWorkspace([type: 'flatgeobuf', file: new File(directory, 'points.fgb').absolutePath])
        assertNotNull geobuf
        assertTrue geobuf.names.contains("points")
    }

}


<|start_filename|>src/test/groovy/geoscript/filter/FunctionTest.groovy<|end_filename|>
package geoscript.filter

import geoscript.feature.Feature
import geoscript.geom.Point
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.layer.Shapefile
import geoscript.style.Fill
import geoscript.style.Stroke
import geoscript.style.Shape
import geoscript.style.Transform
import geoscript.style.Label
import geoscript.style.Font
import geoscript.process.Process
import geoscript.geom.GeometryCollection
import geoscript.layer.Layer

/**
 * The Function UnitTest
 * @author <NAME>
 */
class FunctionTest {

    @TempDir
    private File folder

    @Test void createFromGeoToolsFunction() {
        def f = new Function(Function.ff.function("centroid", Function.ff.property("the_geom")))
        assertNotNull f
        assertNotNull f.function
        assertEquals "centroid", f.function.name
        assertEquals 1, f.function.parameters.size()
        assertEquals "the_geom", f.function.parameters[0].toString()
        assertEquals "centroid([the_geom])", f.toString()
    }

    @Test void createFromCQL() {
        def f = new Function("centroid(the_geom)")
        assertNotNull f
        assertNotNull f.function
        assertEquals "centroid", f.function.name
        assertEquals 1, f.function.parameters.size()
        assertEquals "the_geom", f.function.parameters[0].toString()
        assertEquals "centroid([the_geom])", f.toString()
    }

    @Test void createFromNameAndExpressions() {
        def f = new Function("centroid", new Property("the_geom"))
        assertNotNull f
        assertNotNull f.function
        assertEquals "centroid", f.function.name
        assertEquals 1, f.function.parameters.size()
        assertEquals "the_geom", f.function.parameters[0].toString()
        assertEquals "centroid([the_geom])", f.toString()
    }

    @Test void createFromNameClosureAndExpressions() {
        def f = new Function("my_centroid", {g-> g.centroid}, new Property("the_geom"))
        assertNotNull f
        assertNotNull f.function
        assertEquals "my_centroid", f.function.name
        assertEquals 1, f.function.parameters.size()
        assertEquals "the_geom", f.function.parameters[0].toString()
        assertEquals "my_centroid([the_geom])", f.toString()
    }

    @Test void createFromCqlAndClosure() {
        def f = new Function("my_centroid(the_geom)", {g-> g.centroid})
        assertNotNull f
        assertNotNull f.function
        assertEquals "my_centroid", f.function.name
        assertEquals 1, f.function.parameters.size()
        assertEquals "the_geom", f.function.parameters[0].toString()
        assertEquals "my_centroid([the_geom])", f.toString()
    }

    @Test void evalulate() {
        // Geometry
        Function func = new Function("centroid(the_geom)")
        Feature feature = new Feature([the_geom: new Point(100,100).buffer(2)],"poly1")
        def value = func.evaluate(feature)
        assertTrue value instanceof Point
        assertEquals 100, (value as Point).x, 0.1
        assertEquals 100, (value as Point).y, 0.1
        // String
        func = new Function("strToLowerCase(NAME)")
        feature = new Feature([NAME:"Test"],"test1")
        value = func.evaluate(feature)
        assertEquals "test", value
    }

    @Test void registerFunction() {
        Function.registerFunction("lcase", {str -> str.toLowerCase()})
        def f = new Function("lcase(STATE_ABBR)")
        assertNotNull f
        assertNotNull f.function
        assertEquals "lcase", f.function.name
        assertEquals 1, f.function.parameters.size()
        assertEquals "STATE_ABBR", f.function.parameters[0].toString()
        assertEquals "lcase([STATE_ABBR])", f.toString()
    }

    @Test void rendering() {

        // Register custom Functions
        Function.registerFunction("my_centroid", {g -> g.centroid})
        Function.registerFunction("lcase", {str -> str.toLowerCase()})

        File imgFile = new File(folder, "states_function.png")
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        def statesShp = new Shapefile(file)
        statesShp.style = (new Fill("#E6E6E6") + new Stroke("#4C4C4C",0.5)) +
                (new Shape("#66CCff", 6, "circle").stroke("#004080") + new Transform("my_centroid(the_geom)")).zindex(1) +
                (new Label("STATE_ABBR").font(new Font("normal", "bold", 10, "serif")).fill(new Fill("#004080")) + new Transform("lcase(STATE_ABBR)")).zindex(2)

        def map = new geoscript.render.Map(width: 600, height: 400, fixAspectRatio: true)
        map.proj = "EPSG:4326"
        map.addLayer(statesShp)
        map.bounds = statesShp.bounds
        map.render(imgFile)
    }
    
    @Test void processFunction() {
        Process p = new Process("convexhull",
                "Create a convexhull around the features",
                [features: geoscript.layer.Cursor],
                [result: geoscript.layer.Cursor],
                { inputs ->
                    def geoms = new GeometryCollection(inputs.features.collect{f -> f.geom})
                    def output = new Layer()
                    output.add([geoms.convexHull])
                    [result: output]
                }
        )
        Function f = new Function(p, new Function("parameter", new Expression("features")))

        File imgFile = new File(folder, "states_function.png")
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        def statesShp = new Shapefile(file)

        def sym = (new Stroke("red",0.4) + new Transform(f, Transform.RENDERING)).zindex(1) + (new Fill("#E6E6E6") + new Stroke("#4C4C4C",0.5)).zindex(2)
        assertTrue sym.sld.contains("<ogc:Function name=\"geoscript:convexhull\">")
        statesShp.style = sym

        def map = new geoscript.render.Map(width: 600, height: 400, fixAspectRatio: true)
        map.proj = "EPSG:4326"
        map.addLayer(statesShp)
        map.bounds = statesShp.bounds
        map.render(imgFile)
    }

    @Test void getFunctionNames() {
        List names = Function.functionNames
        assertTrue names.size() > 0
        assertTrue names.contains("Area")
        assertTrue names.contains("cos")
        assertTrue names.contains("within")
    }
}


<|start_filename|>src/main/groovy/geoscript/layer/VectorTiles.groovy<|end_filename|>
package geoscript.layer

import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.geom.Bounds
import geoscript.layer.io.Pbf
import geoscript.layer.io.Reader
import geoscript.layer.io.Writer
import geoscript.layer.io.Readers
import geoscript.layer.io.Writers
import geoscript.proj.Projection
import geoscript.style.Style
import geoscript.workspace.Memory
import geoscript.workspace.Workspace
import groovy.sql.Sql
import org.geotools.map.FeatureLayer
import org.geotools.mbtiles.MBTilesFile
import org.geotools.mbtiles.MBTilesMetadata
import org.geotools.mbtiles.MBTilesTile
import org.geotools.util.logging.Logging

import java.util.logging.Level
import java.util.logging.Logger

/**
 * A TileLayer for VectorTiles
 * @author <NAME>
 */
class VectorTiles extends TileLayer<Tile> implements Renderable {

    /**
     * The File directory
     */
    File dir

    /**
     * The base URL
     */
    URL url

    /**
     * The MBTiles File
     */
    File file

    /**
     * The TileStore used to get, put, and delete Tiles
     */
    private TileStore tileStore

    /**
     * The Pyramid structure
     */
    Pyramid pyramid

    /**
     * The vector tile type (json, pbf)
     */
    String type

    /**
     * The Layer Reader
     */
    Reader reader

    /**
     * Either a Style or a Map of Styles by Layer name
     */
    def style

    /**
     * The Projection of the Layers
     */
    Projection proj

    /**
     * The Logger
     */
    private static final Logger LOGGER = Logging.getLogger("geoscript.layer.VectorTiles");

    /**
     * Create a new VectorTiles TileLayer for a directory
     * @param options The optional named parameters
     * <ul>
     *     <li>proj = The Projection of the vector Layers</li>
     *     <li>style = A Style or a Map of Styles with the Layer name as key</li>
     * </ul>
     * @param name The name of the TileLayer
     * @param file The directory of the Tiles or the MBTiles File
     * @param pyramid The Pyramid
     * @param type The type (pbf, mvt, geojson, kml)
     */
    VectorTiles(Map options = [:], String name, File file, Pyramid pyramid, String type) {
        this.name = name
        this.pyramid = pyramid
        this.bounds = this.pyramid.bounds
        this.type = type
        this.reader = getReaderForType(type)
        this.proj = options.get("proj", pyramid.proj)
        this.style = options.get("style")
        if (file.isDirectory()) {
            this.dir = file
            this.tileStore = new DirectoryTileStore(this.dir, this.type)
        } else {
            this.file = file
            this.tileStore = new MBTilesTileStore(this.file, this.name, "Vector Tiles", format: this.type)
        }
    }

    /**
     * Create a new VectorTiles TileLayer for a URL
     * @param options The optional named parameters
     * <ul>
     *     <li>proj = The Projection of the vector Layers</li>
     *     <li>style = A Style or a Map of Styles with the Layer name as key</li>
     * </ul>
     * @param name The name of the TileLayer
     * @param url The base URL
     * @param pyramid The Pyramid
     * @param type The type (pbf, mvt, geojson, kml)
     */
    VectorTiles(Map options = [:], String name, URL url, Pyramid pyramid, String type) {
        this.name = name
        this.url = url
        this.pyramid = pyramid
        this.bounds = this.pyramid.bounds
        this.type = type
        this.reader = getReaderForType(type)
        this.proj = options.get("proj", pyramid.proj)
        this.style = options.get("style")
        this.tileStore = new UrlTileStore(this.url, this.type)
    }

    private Reader getReaderForType(String type) {
        if (type.equalsIgnoreCase("json")) {
            type = "geojson"
        }
        Readers.find(type)
    }

    /**
     * Get the Pyramid
     * @return The Pyramid
     */
    @Override
    Pyramid getPyramid() {
        pyramid
    }

    /**
     * Get a Tile
     * @param z The zoom level
     * @param x The column
     * @param y The row
     * @return A Tile
     */
    @Override
    Tile get(long z, long x, long y) {
        tileStore.get(z,x,y)
    }

    /**
     * Add a Tile
     * @param t The Tile
     */
    @Override
    void put(Tile t) {
        tileStore.put(t)
    }

    /**
     * Delete a Tile
     * @param t The Tile
     */
    @Override
    void delete(Tile t) {
        tileStore.delete(t)
    }

    /**
     * Close the TileLayer
     */
    @Override
    void close() throws IOException {
        tileStore.close()
    }

    private static interface TileStore {
        Tile get(long z, long x, long y)
        void put(Tile t)
        void delete(Tile t)
        void close() throws IOException
    }

    private static class DirectoryTileStore implements TileStore {

        private File dir

        private String type

        DirectoryTileStore(File dir, String type) {
            this.dir = dir
            this.type = type
        }

        @Override
        Tile get(long z, long x, long y) {
            Tile tile = new Tile(z, x, y)
            File file = new File(new File(new File(this.dir, String.valueOf(z)), String.valueOf(x)), "${y}.${type}")
            if (file.exists()) {
                tile.data = file.bytes
            }
            tile
        }

        @Override
        void put(Tile t) {
            File file = new File(new File(new File(this.dir, String.valueOf(t.z)), String.valueOf(t.x)), "${t.y}.${type}")
            if (!file.parentFile.exists()) {
                file.parentFile.mkdirs()
            }
            file.withOutputStream { out ->
                out.write(t.data)
            }
        }

        @Override
        void delete(Tile t) {
            File file = new File(new File(new File(this.dir, String.valueOf(t.z)), String.valueOf(t.x)), "${t.y}.${type}")
            if (file.exists()) {
                file.delete()
            }
        }

        @Override
        void close() throws IOException {
        }
    }

    private static class UrlTileStore implements TileStore {

        private URL url

        private String type

        UrlTileStore(URL url, String type) {
            this.url = url
            this.type = type
        }

        @Override
        Tile get(long z, long x, long y) {
            Tile tile = new Tile(z, x, y)
            String urlString = url.toString()
            URL tileUrl = new URL("${urlString}${urlString.endsWith("/") ? '' : '/'}${z}/${x}/${y}.${type}")
            tileUrl.withInputStream { input ->
                tile.data = input.bytes
            }
            tile
        }

        @Override
        void put(Tile t) {
            throw new IllegalArgumentException("Vector Tiles with URL are ready only!")
        }

        @Override
        void delete(Tile t) {
            throw new IllegalArgumentException("Vector Tiles with URL are ready only!")
        }

        @Override
        void close() throws IOException {
        }
    }

    private static class MBTilesTileStore implements TileStore {

        /**
         * The MBTiles File
         */
        private final File file

        /**
         * The GeoTools MBTilesFile
         */
        MBTilesFile tiles

        /**
         * The name
         */
        String name

        /**
         * The Bounds
         */
        Bounds bounds

        /**
         * The Projection
         */
        Projection proj

        /**
         * The cached internal Pyramid
         */
        private Pyramid pyramid

        /**
         * The EPSG:4326 Projection
         */
        private Projection latLonProj = new Projection("EPSG:4326")

        /**
         * The EPSG:3857 Projection
         */
        private Projection mercatorProj = new Projection("EPSG:3857")

        /**
         * The world wide Bounds in EPSG:4326
         */
        private Bounds latLonBounds = new Bounds(-179.99, -85.0511, 179.99, 85.0511, latLonProj)

        /**
         * The world wide Bounds in EPSG:3857
         */
        private Bounds mercatorBounds = latLonBounds.reproject(mercatorProj)

        /**
         * The Groovy Sql Connection
         */
        private Sql sql

        /**
         * Create a new MBTilesTileStore with a existing File
         * @param file The existing File
         */
        MBTilesTileStore(File file) {
            this.file = file
            this.tiles = new MBTilesFile(file)
            this.bounds = mercatorBounds
            this.proj = mercatorProj
            this.name = this.tiles.loadMetaData().name
        }

        /**
         * Create a new MBTilesTileStore with a existing File
         * @param file The existing File name
         */
        MBTilesTileStore(String file) {
            this(new File(file))
        }

        /**
         * Create a new MBTilesTileStore with a new File
         * @param options The optional named parameters
         * <ul>
         *     <li>type = The type of layer (base_layer is the default or overlay)</li>
         *     <li>version = The version number (1.0 is the default)</li>
         *     <li>format = The image format (png is the default, jpeg, or pbf)</li>
         *     <li>attribution = The attributes</li>
         * </ul>
         * @param file The new File
         * @param name The name of the layer
         * @param description The description of the layer
         */
        MBTilesTileStore(java.util.Map options = [:], File file, String name, String description) {

            this.file = file
            this.tiles = new MBTilesFile(file)
            this.bounds = mercatorBounds
            this.proj = mercatorProj
            this.name = name

            String type = options.get("type", "base_layer")
            String version = options.get("version", "1.0")
            String format = options.get("format", "png")
            String attribution = options.get("attribution","Created with GeoScript")

            tiles.init()
            MBTilesMetadata metadata = new MBTilesMetadata()
            metadata.name = name
            metadata.description = description
            metadata.formatStr = format
            metadata.version = version
            metadata.typeStr = type
            metadata.bounds = latLonBounds.env
            metadata.attribution = attribution
            tiles.saveMetaData(metadata)
        }

        /**
         * Create a new MBTilesTileStore with a new File
         * @param options The optional named parameters
         * <ul>
         *     <li>type = The type of layer (base_layer is the default or overlay)</li>
         *     <li>version = The version number (1.0 is the default)</li>
         *     <li>format = The image format (png is the default, jpeg, or pbf)</li>
         *     <li>attribution = The attributes</li>
         * </ul>
         * @param file The new File name
         * @param name The name of the layer
         * @param description The description of the layer
         */
        MBTilesTileStore(java.util.Map options = [:], String fileName, String name, String description) {
            this(options, new File(fileName), name, description)
        }

        Pyramid getPyramid() {
            if (!this.pyramid) {
                this.pyramid = Pyramid.createGlobalMercatorPyramid()
            }
            this.pyramid
        }

        @Override
        Tile get(long z, long x, long y) {
            MBTilesTile t = tiles.loadTile(z, x, y)
            new Tile(t.zoomLevel, t.tileColumn, t.tileRow, t.data)
        }

        @Override
        void put(Tile t) {
            MBTilesTile tile = new MBTilesTile(t.z, t.x, t.y)
            tile.data = t.data
            tiles.saveTile(tile)
        }

        @Override
        void delete(Tile t) {
            getSql().execute("DELETE FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?", [t.z, t.x, t.y])
        }

        /**
         * Delete all Tiles in the TileCursor
         * @param tiles The TileCursor
         */
        void delete(TileCursor<Tile> tiles) {
            getSql().execute("DELETE FROM tiles WHERE zoom_level = ? AND tile_column >= ? AND tile_column <= ? " +
                    "AND tile_row >= ? AND tile_row <= ?", [tiles.z, tiles.minX, tiles.maxX, tiles.minY, tiles.maxY])
        }

        /**
         * Lazily create the Groovy Sql Connection
         * @return
         */
        private Sql getSql() {
            if (!sql) {
                sql = Sql.newInstance("jdbc:sqlite:${file.absolutePath}", "org.sqlite.JDBC")
            }
            sql
        }

        @Override
        void close() throws IOException {
            this.tiles.close()
            if (sql) sql.close()
        }

        /**
         * Get metadata (type, name, description, format, version, attribution, bounds)
         * @return A Map of metadata
         */
        Map<String,String> getMetadata() {
            MBTilesMetadata metadata = tiles.loadMetaData()
            [
                    type: metadata.typeStr,
                    name: metadata.name,
                    description: metadata.description,
                    format: metadata.formatStr,
                    version: metadata.version,
                    attribution: metadata.attribution,
                    bounds: metadata.boundsStr
            ]
        }

        /**
         * Get the number of tiles per zoom level.
         * @return A List of Maps with zoom, tiles, total, and percent keys
         */
        List<Map> getTileCounts() {
            List stats = []
            getSql().eachRow("select count(*) as num_tiles, zoom_level from tiles group by zoom_level order by zoom_level", { def row ->
                long zoom = row.zoom_level
                long numberOfTiles = row.num_tiles
                long totalNumberOfTiles = this.pyramid.grid(row.zoom_level).size
                double percent = totalNumberOfTiles / numberOfTiles
                stats.add([
                        zoom: zoom,
                        tiles: numberOfTiles,
                        total: totalNumberOfTiles,
                        percent: percent
                ])
            })
            stats
        }

        /**
         * Get the maximum zoom level of the tiles present.
         * @return The maximum zoom level of the tile present
         */
        int getMaxZoom() {
            int max
            getSql().eachRow("select max(zoom_level) as max_zoom_level from tiles", {  def row ->
                max = row.max_zoom_level
            })
            max
        }

        /**
         * Get the minimum zoom level of the tiles present.
         * @return The minimum zoom level of the tile present
         */
        int getMinZoom() {
            int min
            getSql().eachRow("select min(zoom_level) as min_zoom_level from tiles", {  def row ->
                min = row.min_zoom_level
            })
            min
        }
    }

    /**
     * Get a List of Layers for the Tiles in the TileCursor
     * @param cursor The TileCursor
     * @return A List of Layers
     */
    List<Layer> getLayers(TileCursor<Tile> cursor) {
        Map layers = [:]
        if (!cursor.empty) {
            cursor.each { Tile t ->
                if (type.equalsIgnoreCase("pbf")) {
                    try {
                        List<Layer> layerList = Pbf.read(t.data, cursor.tileLayer.pyramid.bounds(t))
                        layerList.each { Layer tileLayer ->
                            // Create Schema if necessary
                            if (!layers.containsKey(tileLayer.name)) {
                                Workspace workspace = new Memory()
                                Layer layer = workspace.create(tileLayer.schema.reproject(this.proj, tileLayer.name))
                                layers.put(tileLayer.name, layer)
                            }
                            // Add Features
                            Layer layer = layers.get(tileLayer.name)
                            layer.withWriter { geoscript.layer.Writer w ->
                                tileLayer.eachFeature { Feature f ->
                                    w.add(f)
                                }
                            }
                        }
                    } catch (Exception ex) {
                        LOGGER.log(Level.WARNING, "Can't read ${t}!", ex)
                    }
                } else {
                    try {
                        Layer tileLayer = reader.read(new ByteArrayInputStream(t.data))
                        // Create Schema if necessary
                        if (layers.isEmpty()) {
                            Workspace workspace = new Memory()
                            Layer layer = workspace.create(tileLayer.schema.reproject(this.proj, this.name))
                            layers.put(this.name, layer)
                        }
                        // Add Features
                        Layer layer = layers.get(this.name)
                        layer.withWriter { geoscript.layer.Writer w ->
                            tileLayer.eachFeature { Feature f ->
                                w.add(f)
                            }
                        }
                    } catch (Exception ex) {
                        LOGGER.log(Level.WARNING, "Can't read ${t}!", ex)
                    }
                }
            }
        }
        // Add styles
        layers.each { String name, Layer layer ->
            if (style) {
                if (style instanceof Style) {
                    layer.style = style
                } else {
                    layer.style = style[name]
                }
            }
        }
        layers.values().collect { it }
    }

    /**
     * The VectorTiles TileLayerFactory
     */
    static class Factory extends TileLayerFactory<VectorTiles> {

        @Override
        VectorTiles create(Map params) {
            String type = params.get("type","").toString()
            if (type.equalsIgnoreCase("vectortiles")) {
                String name = params.get("name", "vectortiles")
                Object p = params.get("pyramid", Pyramid.createGlobalMercatorPyramid())
                Pyramid pyramid = p instanceof Pyramid ? p as Pyramid : Pyramid.fromString(p as String)
                String format = params.get("format", "pbf")
                if (params.containsKey("file")) {
                    File file = params["file"] instanceof File ? params["file"] as File : new File(params["file"])
                    new VectorTiles(name, file, pyramid, format)
                } else {
                    URL url = params["url"] instanceof URL ? params["url"] as URL : new URL(params["url"])
                    new VectorTiles(name, url, pyramid, format)
                }
            } else {
                null
            }
        }

        @Override
        TileRenderer getTileRenderer(Map options, TileLayer tileLayer, List<Layer> layers) {
            if (tileLayer instanceof VectorTiles) {
                VectorTiles vectorTiles = tileLayer as VectorTiles
                if (vectorTiles.type.equalsIgnoreCase("pbf")) {
                    Map<String, List> fields = options.get("fields", [:])
                    if (fields.isEmpty()) {
                        layers.each { Layer layer ->
                            fields[(layer.name)] = layer.schema.fields
                        }
                    }
                    new PbfVectorTileRenderer(layers, fields)
                } else {
                    String type = vectorTiles.type
                    if (type.equalsIgnoreCase("json")) {
                        type = "geojson"
                    }
                    Writer layerWriter = Writers.find(type)
                    Layer layer = layers[0]
                    List<Field> fields = options.fields ?
                            options.fields.collect {
                                it instanceof Field ? it : layer.schema.get(it)
                            } : layer.schema.fields
                    new VectorTileRenderer(layerWriter, layer, fields)
                }
            } else {
                null
            }
        }
    }

    @Override
    List<org.geotools.map.Layer> getMapLayers(Bounds bounds, List size) {
        this.getLayers(this.tiles(bounds.reproject(this.proj), size[0], size[1])).collect { Layer lyr ->
            new FeatureLayer(lyr.fs, lyr.style.gtStyle)
        }
    }
}


<|start_filename|>src/test/groovy/geoscript/geom/CompoundCurveTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The CompoundCurve Unit Test
 * @author <NAME>
 */
class CompoundCurveTest {

    @Test void createWithRepeatedLineStrings() {
        CompoundCurve cc = new CompoundCurve(
                new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]),
                new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [5.0, 5.0])
        )
        assertFalse cc.isClosed()
        assertTrue cc.isCurved()
        assertEquals "CompoundCurve", cc.geometryType
        assertEquals "COMPOUNDCURVE (CIRCULARSTRING (10.0 10.0, 0.0 20.0, -10.0 10.0), (-10.0 10.0, -10.0 0.0, 10.0 0.0, 5.0 5.0))", cc.curvedWkt
        assertTrue cc.wkt.startsWith("LINESTRING")
        List lines = cc.components
        assertEquals 2, lines.size()
        assertEquals new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]), lines[0]
        assertEquals new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [5.0, 5.0]), lines[1]
        LineString line = cc.linear
        assertTrue line.numPoints > 7
    }

    @Test void createWithListOfLineStrings() {
        CompoundCurve cc = new CompoundCurve([
                new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]),
                new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [5.0, 5.0])
        ])
        assertFalse cc.isClosed()
        assertTrue cc.isCurved()
        assertEquals "CompoundCurve", cc.geometryType
        assertEquals "COMPOUNDCURVE (CIRCULARSTRING (10.0 10.0, 0.0 20.0, -10.0 10.0), (-10.0 10.0, -10.0 0.0, 10.0 0.0, 5.0 5.0))", cc.curvedWkt
        assertTrue cc.wkt.startsWith("LINESTRING")
        List lines = cc.components
        assertEquals 2, lines.size()
        assertEquals new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]), lines[0]
        assertEquals new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [5.0, 5.0]), lines[1]
        LineString line = cc.linear
        assertTrue line.numPoints > 7
    }

}


<|start_filename|>src/test/groovy/geoscript/feature/io/StringSchemaReaderTest.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Schema
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The StringSchemaReader Unit Test
 * @author <NAME>
 */
class StringSchemaReaderTest {

    @Test void readWithDefaults() {
        String str = "geom:Point:srid=4326,name:String,price:float"
        SchemaReader reader = new StringSchemaReader()
        Schema schema = reader.read(str)
        assertEquals "layer", schema.name
        assertEquals "http://geoscript.org/feature", schema.uri
        assertEquals schema.field("geom").typ, 'Point'
        assertEquals schema.field("geom").proj.id, 'EPSG:4326'
        assertEquals schema.field("name").typ, 'String'
        assertEquals schema.field("price").typ, 'Float'
    }

    @Test void readWithOptions() {
        String str = "geom:Point:srid=4326,name:String,price:float"
        SchemaReader reader = new StringSchemaReader()
        Schema schema = reader.read(name: 'points', uri: 'https://data.org', str)
        assertEquals "points", schema.name
        assertEquals "https://data.org", schema.uri
        assertEquals schema.field("geom").typ, 'Point'
        assertEquals schema.field("geom").proj.id, 'EPSG:4326'
        assertEquals schema.field("name").typ, 'String'
        assertEquals schema.field("price").typ, 'Float'
    }
}


<|start_filename|>src/main/groovy/geoscript/layer/Format.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import geoscript.proj.Projection
import org.geotools.coverage.grid.GridEnvelope2D
import org.geotools.coverage.grid.GridGeometry2D
import org.geotools.coverage.grid.io.AbstractGridFormat
import org.geotools.coverage.grid.io.GridFormatFinder
import org.geotools.coverage.grid.io.UnknownFormat
import org.geotools.util.factory.GeoTools
import org.geotools.util.factory.Hints
import org.opengis.coverage.grid.GridCoverageReader
import org.opengis.parameter.GeneralParameterValue
import org.opengis.parameter.ParameterValueGroup

import java.awt.*
import java.util.List

/**
 * A Raster Format can read and write Rasters
 * @author Jared Erickson
 */
class Format {

    /**
     * The GeoTools AbstractGridFormat
     */
    AbstractGridFormat gridFormat

    /**
     * The stream which is usually a File
     */
    protected def stream

    /**
     * Create a new Format wrapping an AbstractGridFormat
     * @param gridFormat The GeoTools AbstractGridFormat
     * @param stream The input or output stream
     */
    Format(AbstractGridFormat gridFormat, def stream) {
        this.gridFormat = gridFormat
        this.stream = stream
    }

    /**
     * Get the format name
     * @return The format name
     */
    String getName() {
        gridFormat.name
    }

    /**
     * Write the Raster to the destination object (usually a File)
     * @param raster The Raster to write
     */
    void write(Map options = [:], Raster raster) {
        def writer = gridFormat.getWriter(stream)
        try {
            ParameterValueGroup params = writer.format.writeParameters
            options.each{k,v ->
                params.parameter(k).setValue(v)
            }
            def gpv = options.collect{k,v ->
                params.parameter(k)
            }
            writer.write(raster.coverage, gpv as GeneralParameterValue[])
        } finally {
            writer.dispose()
        }
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @return A Raster
     */
    Raster read(Map options = [:]) {
        this.read(options, GeoTools.getDefaultHints())
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param name The name of the Raster to read
     * @return A Raster
     */
    Raster read(Map options = [:], String name) {
        this.read(options, name, GeoTools.getDefaultHints())
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param proj The Projection
     * @return A Raster
     */
    Raster read(Map options = [:], Projection proj) {
        this.read(options, "", proj)
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param name The name of the Raster
     * @param proj The Projection
     * @return A Raster
     */
    Raster read(Map options = [:], String name, Projection proj) {
        // Create Hints
        Hints hints = GeoTools.getDefaultHints()
        if (proj) {
            hints.put(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM, proj.crs)
        }
        this.read(options, name, hints)
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param hints GeoTools Hints
     * @return A Raster
     */
    Raster read(Map options = [:],  Hints hints) {
        this.read(options, "", hints)
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param name The name of the Raster
     * @param hints GeoTools Hints
     * @return A Raster
     */
    Raster read(Map options = [:], String name, Hints hints) {
        Raster raster = null
        // Create Reader
        def reader = gridFormat.getReader(stream, hints)
        try {
            // Check for ReadGridGeometry2D parameters
            if (options.containsKey("bounds")) {
                Bounds bounds = options.bounds
                List size = options.containsKey("size") ? options.size : [500,500]
                options.put("ReadGridGeometry2D", new GridGeometry2D(
                    new GridEnvelope2D(new Rectangle(size[0], size[1])),
                    bounds.env
                ))
                options.remove("bounds")
                options.remove("size")
            }
            // Create GeneralParameterValues
            ParameterValueGroup params = reader.format.readParameters
            options.each{k,v ->
                params.parameter(k).setValue(v)
            }
            def gpv = options.collect{k,v ->
                params.parameter(k)
            }
            // Read the Raster
            if (name) {
                raster = new Raster(reader.read(name, gpv as GeneralParameterValue[]), this)
            } else {
                raster = new Raster(reader.read(gpv as GeneralParameterValue[]), this)
            }
        } finally {
            reader.dispose()
        }
        raster
    }

    /**
     * Get the list of the raster names
     * @return A List of the raster names
     */
    List<String> getNames() {
        List names = []
        GridCoverageReader reader = this.gridFormat.getReader(stream)
        try {
            names.addAll(reader.gridCoverageNames)
        } finally {
            reader.dispose()
        }
        names
    }

    /**
     * Whether the Format has a Raster by the given name
     * @param name The Raster name
     * @return Whether the Format has a Raster by the given name
     */
    boolean has(String name) {
        getNames().contains(name)
    }

    /**
     * Get metadata
     * @param name The optional Raster name
     * @return A Map of metadata
     */
    Map getMetadata(String name = null) {
        Map metadata = [:]
        GridCoverageReader reader = this.gridFormat.getReader(stream)
        try {
            reader.metadataNames.each { String metadataName ->
                metadata[metadataName] = name ?
                        reader.getMetadataValue(name, metadataName) : reader.getMetadataValue(metadataName)
            }
        } finally {
            reader.dispose()
        }
        metadata
    }

    /**
     * Get a String representation of this Format
     * @return The Format's name
     */
    String toString() {
        name
    }

    /**
     * Get the Format that can read the given input stream
     * @param input The input stream (which is usually a File, but can be a URL, InputStream, or connection string)
     * @return A Format
     */
    static Format getFormat(Object input) {
        Format format = null
        // Try with FormatFactories
        for (FormatFactory formatFactory : FormatFactories.list()) {
            format = formatFactory.create(input)
            if (format != null) {
                break
            }
        }
        // Then try with GeoTools GridFormatFinder
        if (!format) {
            AbstractGridFormat gridFormat = null
            try {
                gridFormat = GridFormatFinder.findFormat(input)
            } catch(Exception ex) {
                // Move on
            }
            if (gridFormat && !(gridFormat instanceof UnknownFormat)) {
                format = new Format(gridFormat, input)
            }
        }
        format
    }
}


<|start_filename|>src/main/groovy/geoscript/geom/LinearRing.groovy<|end_filename|>
package geoscript.geom

import org.locationtech.jts.geom.LinearRing as JtsLinearRing
import org.locationtech.jts.geom.Coordinate

/**
 * A LinearRing Geometry is a LineString whose first and last coordinates are the same forming a closed ring.
 * <p>You can create a LinearRing with a List of List of Doubles:</p>
 * <p><blockquote><pre>
 * LinearRing l = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47], [111.0, -47]])
 * </pre></blockquote></p>
 * <p>Or you can create a LinearRing wih a List of {@link Point}s:</p>
 * <p><blockquote><pre>
 * LinearRing l = new LinearRing([new Point(111.0, -47), new Point(123.0, -48), new Point(110.0, -47), new Point(111.0, -47)])
 * </pre></blockquote></p>
 * <p>Or you can create a LinearRing with a repeated List of Doubles:</p>
 * <p><blockquote><pre>
 * LinearRing l =  new LinearRing([111.0, -47],[123.0, -48],[110.0, -47], [111.0, -47])
 * </pre></blockquote></p>
 * <p>Or you can create a LinearRing with a repated List of {@link Point}s:</p>
 * <p><blockquote><pre>
 * LinearRing l = new LinearRing(new Point(111.0, -47), new Point(123.0, -48), new Point(110.0, -47), new Point(111.0, -47))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class LinearRing extends LineString {
	
    /**
     * Create a LinearRing by wrapping a JTS LinearRing
     * @param ring A JTS LinearRing
     */
    LinearRing (JtsLinearRing ring) {
        super(ring)
    }
	
    /**
     * Create a new LinearRing with a List of List of Doubles.
     * <p><blockquote><pre>
     * LinearRing ring = new LinearRing([[1,2],[3,4],[4,5]])
     * LinearRing ring = new LinearRing([new Point(111.0, -47), new Point(123.0, -48), new Point(110.0, -47), new Point(111.0, -47)])
     * </pre></blockquote></p>
     * @param coordinate A List of List Double or a List of Points
     */
    LinearRing(List coordinates) {
        this(create(coordinates))
    }

    /**
     * Create a new LinearRing with a List of List of Doubles.
     * <p><blockquote><pre>
     * LinearRing ring = new LinearRing([1,2],[3,4],[4,5])
     * </pre></blockquote></p>
     * @param coordinates A repeated List of List of Doubles
     *
     */
    LinearRing(List<Double>... coordinates) {
        this(create(coordinates))
    }

    /**
     * Create a new LinearRing with a repeated List {@link Point}s.
     * <p><blockquote><pre>
     * LinearRing ring = new LinearRing(new Point(1,2), new Point(3,4), new Point(4,5))
     * </pre></blockquote></p>
     * @param points A repeated List of Points
     */
    LinearRing(Point... points) {
        this(create(points))
    }

    /**
     * Create a new LinearRing with a List of List of Doubles.
     * <p><blockquote><pre>
     * LinearRing ring = new LinearRing([[1,2],[3,4],[4,5]])
     * </pre></blockquote></p>
     * @param coordinate A List of List of Doubles or a List of Points
     */
    private static JtsLinearRing create(List coordinates) {
        Geometry.factory.createLinearRing(coordinates.collect{c ->
                (c instanceof Point) ? c.g.coordinate : new Coordinate(c[0], c[1])
        }.toArray() as Coordinate[])
    }
	
    /**
     * Create a new LinearRing with a List of List of Doubles.
     * <p><blockquote><pre>
     * LinearRing ring = new LinearRing([1,2],[3,4],[4,5])
     * </pre></blockquote></p>
     * @param coordinates A repeated List of Doubles
     */
    private static JtsLinearRing create(List<Double>... coordinates) {
        Geometry.factory.createLinearRing(coordinates.collect{new Coordinate(it[0], it[1])}.toArray() as Coordinate[])
    }

    /**
     * Create a new LinearRing with a repeated List {@link Point}s.
     * <p><blockquote><pre>
     * LinearRing ring = new LinearRing(new Point(1,2), new Point(3,4), new Point(4,5))
     * </pre></blockquote></p>
     * @param points A repeated List of Points
     */
    private static JtsLinearRing create(Point... points) {
        Geometry.factory.createLinearRing(points.collect{it.g.coordinate}.toArray() as Coordinate[])
    }
	
}

<|start_filename|>src/test/groovy/geoscript/plot/ScatterTest.groovy<|end_filename|>
package geoscript.plot

import geoscript.geom.Bounds
import geoscript.geom.Geometry
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import static org.junit.jupiter.api.Assertions.*

/**
 * The Scatter Unit Test
 * @author <NAME>
 */
class ScatterTest {

    @TempDir
    File folder

    @Test void scatterplot() {
        def points = Geometry.createRandomPoints(new Bounds(0,0,100,100).geometry, 10)
        List data = points.geometries.collect{pt ->
            [pt.x,pt.y]
        }
        Chart chart = Scatter.scatterplot(data)
        File file = new File(folder,"scatterplot.png")
        chart.save(file)
        assertTrue(file.exists())
    }

}


<|start_filename|>src/main/groovy/geoscript/feature/io/GmlWriter.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Feature
import javax.xml.namespace.QName
import org.geotools.xsd.Encoder
import org.geotools.gml2.GMLConfiguration as GML2
import org.geotools.gml3.GMLConfiguration as GML3
import org.geotools.gml3.v3_2.GMLConfiguration as GML32


/**
 * Write a Feature to GML.
 * <p><blockquote><pre>
 * {@link geoscript.feature.Schema Schema} schema = new {@link geoscript.feature.Schema Schema}("houses", [new {@link geoscript.feature.Field Field}("geom","Point"), new {@link geoscript.feature.Field Field}("name","string"), new {@link geoscript.feature.Field Field}("price","float")])
 * {@link geoscript.feature.Feature Feature} feature = new {@link geoscript.feature.Feature Feature}([new Point(111,-47), "House", 12.5], "house1", schema)
 * GmlWriter writer = new GmlWriter()
 * String gml = writer.write(feature)
 *
 * &lt;gml:houses fid="house1" xmlns:gml="http://www.opengis.net/gml" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
 *    &lt;gml:name&gt;House&lt;/gml:name&gt;
 *    &lt;gml:geom&gt;
 *       &lt;gml:Point&gt;
 *           &lt;gml:coord&gt;
 *               &lt;gml:X&gt;111.0&lt;/gml:X&gt;
 *               &lt;gml:Y&gt;-47.0&lt;/gml:Y&gt;
 *           &lt;/gml:coord&gt;
 *       &lt;/gml:Point&gt;
 *    &lt;/gml:geom&gt;
 *    &lt;gml:price&gt;12.5&lt;/gml:price&gt;
 * &lt;/gml:houses&gt;
 * </pre></blockquote></p>
 * @author <NAME>
 */
class GmlWriter implements Writer {

    /**
     * Write a Feature to a GML String
     * @param feature The Feature
     * @return A GML String
     */
    String write(Feature feature) {
        write(feature, 2, true, false, false, "gsf")
    }

    /**
     * Write the Feature to a GML String
     * @param feature The Feature
     * @param version The version 2, 3, or 3.2
     * @param format Whether to pretty print or not
     * @param bounds Whether to include Feature Bounds or not
     * @param xmldecl Whether to include XML declaration or not
     * @param nsprefix The XML namespace prefix
     * @return A GML String
     */
    String write(Feature feature, double version, boolean format, boolean bounds, boolean xmldecl, String nsprefix) {
        String nsURI = (version == 3.2) ? new GML32().getNamespaceURI() : new GML3().getNamespaceURI()
        String fName = (version < 3.2) ? "_Feature" : "AbstractFeature"
        QName qName = new QName(nsURI, fName)
        def gmlConfig = getGmlConfig(version)
        if (!bounds) {
            gmlConfig.getProperties().add(GML2.NO_FEATURE_BOUNDS)
        }
        Encoder encoder = new Encoder(gmlConfig)
        if (!xmldecl) {
            encoder.omitXMLDeclaration = true
        }
        if (format) {
            encoder.indenting = true
        }
        encoder.namespaces.declarePrefix(nsprefix, feature.schema.uri)

        encoder.encodeAsString(feature.f, qName)
    }

    private def getGmlConfig(double version) {
        if (version == 2) {
            return new GML2()
        } else if (version == 3) {
            return new GML3()
        } else /*if (version == 3.2)*/ {
            return new GML32()
        }
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/ScaleBarItem.groovy<|end_filename|>
package geoscript.carto

import geoscript.render.Map
import org.geotools.renderer.lite.RendererUtilities

import java.awt.Color
import java.awt.Font

/**
 * Adds scale bar to a cartographic document.
 * @author <NAME>
 */
class ScaleBarItem extends Item {

    Map map

    Color strokeColor = Color.BLACK

    Color fillColor = Color.WHITE

    float strokeWidth = 1

    Font font = new Font("Arial", Font.PLAIN, 12)

    int border = 5

    Units units = Units.METRIC

    enum Units {
        METRIC,
        US
    }

    /**
     * Create a scale bar from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    ScaleBarItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the Map to use when calculating the map scale
     * @param map The Map
     * @return The ScaleBarItem
     */
    ScaleBarItem map(Map map) {
        this.map = map
        this
    }

    /**
     * Set the Units (US or METRIC) to use
     * @param units Units (US or METRIC)
     * @return The ScaleBarItem
     */
    ScaleBarItem units(Units units) {
        this.units = units
        this
    }

    /**
     * Set stroke Color
     * @param strokeColor The stroke Color
     * @return The ScaleBarItem
     */
    ScaleBarItem strokeColor(Color strokeColor) {
        this.strokeColor = strokeColor
        this
    }

    /**
     * Set the fill Color
     * @param fillColor The fill Color
     * @return The ScaleBarItem
     */
    ScaleBarItem fillColor(Color fillColor) {
        this.fillColor = fillColor
        this
    }

    /**
     * Set the stroke width
     * @param strokeWidth The stroke width
     * @return The ScaleBarItem
     */
    ScaleBarItem strokeWidth(float strokeWidth) {
        this.strokeWidth = strokeWidth
        this
    }

    /**
     * Set the Font
     * @param font The Font
     * @return The ScaleBarItem
     */
    ScaleBarItem font(Font font) {
        this.font = font
        this
    }

    /**
     * Set the border padding
     * @param border The border padding
     * @return The ScaleBarItem
     */
    ScaleBarItem border(int border) {
        this.border = border
        this
    }

    /**
     * Calculate the scale bar information
     * @return A ScaleBarInfo instance
     */
    ScaleBarInfo calculateScaleBarInfo() {
        
        double widthInMeters = RendererUtilities.toMeters(map.bounds.width, map.bounds.proj.crs)
        double widthInUnits = widthInMeters
        if (units == ScaleBarItem.Units.US) {
            widthInUnits = org.geotools.measure.Units.METRE.getConverterTo(org.geotools.measure.Units.FOOT).convert(widthInMeters)
        }

        double scaleDenominator = map.scaleDenominator
        double pixelsPerMeter = RendererUtilities.calculatePixelsPerMeterRatio(scaleDenominator, [:])
        String unitForScaleText
        double pixelsPerUnit
        if (units == ScaleBarItem.Units.US) {
            if (widthInUnits > 5280) {
                unitForScaleText = "miles"
                pixelsPerUnit = pixelsPerMeter / 0.000621371
            } else {
                unitForScaleText = "feet"
                pixelsPerUnit = pixelsPerMeter / 3.28084
            }
        } else {
            if (widthInUnits > 1000) {
                unitForScaleText = "km"
                pixelsPerUnit = pixelsPerMeter * 1000
            } else {
                unitForScaleText = "m"
                pixelsPerUnit = pixelsPerMeter
            }
        }

        int scaleBarWidthInPixels = width - border * 2
        double scaleBarWidthInUnits = scaleBarWidthInPixels / pixelsPerUnit

        double orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(scaleBarWidthInUnits)))
        double desiredScaleBarWidthInUnits = Math.round(scaleBarWidthInUnits / orderOfMagnitude) * orderOfMagnitude
        double desiredScaleBarWidthInPixels = Math.round(desiredScaleBarWidthInUnits * pixelsPerUnit)

        new ScaleBarInfo(
            widthInPixels: desiredScaleBarWidthInPixels,
            widthInUnits: desiredScaleBarWidthInUnits,
            unitForScaleText: unitForScaleText
        )
    }

    static class ScaleBarInfo {

        double widthInPixels

        double widthInUnits

        String unitForScaleText

    }

}


<|start_filename|>src/test/groovy/geoscript/plot/CurveTest.groovy<|end_filename|>
package geoscript.plot

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import static org.junit.jupiter.api.Assertions.*

/**
 * The Curve Unit Test
 * @author <NAME>
 */
class CurveTest {

    @TempDir
    File folder

    @Test void curve() {
        List data = [
                [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart = Curve.curve(data)
        File file = new File(folder,"curve.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void curveSmooth() {
        List data = [
                [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart = Curve.curve(data, smooth: true)
        File file = new File(folder,"curveSmooth.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void curve3D() {
        List data = [
                [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart = Curve.curve(data, trid: true)
        File file = new File(folder,"curve3d.png")
        chart.save(file)
        assertTrue(file.exists())
    }

}


<|start_filename|>src/main/groovy/geoscript/viewer/Viewer.groovy<|end_filename|>
package geoscript.viewer

import org.locationtech.jts.geom.Geometry as JtsGeometry

import org.locationtech.jts.awt.PointShapeFactory
import org.locationtech.jts.awt.PointTransformation
import org.locationtech.jts.awt.ShapeWriter
import org.locationtech.jts.geom.Coordinate
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.geom.GeometryCollection

import java.awt.geom.Point2D
import java.awt.image.BufferedImage
import java.util.List
import javax.imageio.ImageIO
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.WindowConstants
import java.awt.*

/**
 * A Viewer can be used to visualize Geometry.
 * <p><blockquote><pre>
 * import geoscript.geom.Point
 * import geoscript.viewer.Viewer
 * Point p = new Point(10,10)
 * Viewer.draw(p.buffer(100))
 * </pre></blockquote></p>
 * Or you can plot a List of Geometries.
 * <p><blockquote><pre>
 * import geoscript.geom.Point
 * import geoscript.viewer.Viewer
 * Point p = new Point(10,10)
 * Viewer.plot([p, p.buffer(50), p.buffer(100)])
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Viewer {

    /**
     * Draw a Geometry (or List of Geometries) onto a GUI
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the image</li>
     *  <li>bounds = The Bounds</li>
     *  <li>strokeColor = The stroke color</li>
     *  <li>fillColor = The fill color</li>
     *  <li>markerShape = The marker shape (circle, square, cross, ect...)</li>
     *  <li>markerSize = The marker size</li>
     *  <li>opacity = The opacity</li>
     *  <li>strokeWidth = The stroke width</li>
     *  <li>drawCoords = Whether to draw coordinates or not (true | false)</li>
     * </ul>
     * @param geom The Geometry to List of Geometries to render
     */
    static void draw(java.util.Map options = [:], def geom) {
        if (!(geom instanceof java.util.List)) {
            geom = [geom]
        }
        Panel panel = new Panel(options, geom)
        java.util.List size = options.get("size", [500,500])
        double buf = options.get("expandBy", 0)
        Dimension dim = new Dimension((int) (size[0] + 2 * buf), (int) (size[1] + 2 * buf))
        panel.preferredSize = dim
        panel.minimumSize = dim
        JFrame frame = new JFrame("GeoScript Viewer")
        // If we are opening Windows from the GroovyConsole, we can't use EXIT_ON_CLOSE because the GroovyConsole
        // itself will exit
        if (java.awt.Frame.frames.find{it.title.contains("GroovyConsole")}) {
            frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
        } else {
            // The Groovy Shell has a special SecurityManager that doesn't allow EXIT_ON_CLOSE
            try { frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE } catch (SecurityException ex) {frame.defaultCloseOperation = JFrame.HIDE_ON_CLOSE}
        }
        frame.layout = new BorderLayout()
        frame.add(panel, BorderLayout.CENTER)
        frame.pack()
        frame.visible = true
    }

    /**
     * Draw Geometry (or List of Geometries) to a BufferedImage
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the image</li>
     *  <li>bounds = The Bounds</li>
     *  <li>strokeColor = The stroke color</li>
     *  <li>fillColor = The fill color</li>
     *  <li>markerShape = The marker shape (circle, square, cross, ect...)</li>
     *  <li>markerSize = The marker size</li>
     *  <li>opacity = The opacity</li>
     *  <li>strokeWidth = The stroke width</li>
     *  <li>drawCoords = Whether to draw coordinates or not (true | false)</li>
     * </ul>
     * @param geom A Geometry or a List of Geometries
     * @return A BufferedImage
     */
    static BufferedImage drawToImage(java.util.Map options = [:], def geom) {
        java.util.List size = options.get("size", [500,500])
        BufferedImage image = new BufferedImage(size[0], size[1], BufferedImage.TYPE_INT_ARGB)
        Graphics2D g2d = image.createGraphics()
        Bounds bounds = new GeometryCollection(geom).bounds
        bounds.expandBy(bounds.width * 0.10)
        geom = geom instanceof java.util.List ? geom : [geom]
        draw(options, g2d, geom)
        g2d.dispose()
        image
    }

    /**
     * Draw Geometry (or List of Geometries) to a Base64 Encoded String
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the image</li>
     *  <li>bounds = The Bounds</li>
     *  <li>strokeColor = The stroke color</li>
     *  <li>fillColor = The fill color</li>
     *  <li>markerShape = The marker shape (circle, square, cross, ect...)</li>
     *  <li>markerSize = The marker size</li>
     *  <li>opacity = The opacity</li>
     *  <li>strokeWidth = The stroke width</li>
     *  <li>drawCoords = Whether to draw coordinates or not (true | false)</li>
     *  <li>includePrefix = Whether to include the base 64 prefix</li>
     *  <li>imageType = The image type (PNG or JPEG)</li>
     * </ul>
     * @param geom A Geometry or a List of Geometries
     * @return A Base64 Encoded String
     */
    static String drawToBase64EncodedString(java.util.Map options = [:], def geom) {
        BufferedImage image = drawToImage(options, geom)
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        String imageType = options.get("imageType", "png")
        ImageIO.write(image, imageType, out)
        byte[] bytes = org.apache.commons.codec.binary.Base64.encodeBase64(out.toByteArray())
        String str = new String(bytes, "UTF-8")
        if (options.get("includePrefix", true)) {
            str = "image/${imageType};base64,${str}"
        }
        str
    }

    /**
     * Save a drawing of the Geometry (or List of Geometries) to a File
     * @param options A Map of options or named parameters
     * @param geom A Geometry or a List of Geometries
     * @param file The File
     */
    static void drawToFile(java.util.Map options = [:], def geom, File file) {
        String fileName = file.absolutePath
        String imageFormat = fileName.substring(fileName.lastIndexOf(".")+1)
        FileOutputStream out = new FileOutputStream(file)
        ImageIO.write(drawToImage(options, geom), imageFormat, out)
        out.close()
    }

    /**
     * Draw a List of GeoScript Geometries to a Graphics2D.
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the image</li>
     *  <li>bounds = The Bounds</li>
     *  <li>strokeColor = The stroke color</li>
     *  <li>fillColor = The fill color</li>
     *  <li>markerShape = The marker shape (circle, square, cross, ect...)</li>
     *  <li>markerSize = The marker size</li>
     *  <li>opacity = The opacity</li>
     *  <li>strokeWidth = The stroke width</li>
     *  <li>drawCoords = Whether to draw coordinates or not (true | false)</li>
     * </ul>
     * @param g2d The Graphics2D
     * @param geometries A List of GeoScript Geometries
     */
    static void draw(java.util.Map options = [:], Graphics2D g2d, List geometries) {

        java.util.List size = options.get("size", [500,500])
        Bounds bounds = (options.get("bounds",new GeometryCollection(geometries).bounds.scale(1.1)) as Bounds).ensureWidthAndHeight()
        java.awt.Color strokeColor = new geoscript.filter.Color(options.get("strokeColor", [99,99,99])).asColor()
        java.awt.Color fillColor = new geoscript.filter.Color(options.get("fillColor", [206,206,206])).asColor()
        java.awt.Color backgroundColor = options.get("backgroundColor", null) == null ? null : new geoscript.filter.Color(options.get("backgroundColor")).asColor()
        String markerShape = options.get("markerShape","square")
        double markerSize = options.get("markerSize", 8)
        float opacity = options.get("opacity", 0.75)
        float strokeWidth = options.get("strokeWidth", 1.0)
        boolean drawCoordinates = options.get("drawCoords", false)

        int imageWidth = size[0]
        int imageHeight = size[1]

        def shapeFactory
        if (markerShape.equalsIgnoreCase("circle")) {
            shapeFactory = new PointShapeFactory.Circle(markerSize)
        } else if (markerShape.equalsIgnoreCase("cross")) {
            shapeFactory = new PointShapeFactory.Cross(markerSize)
        } else if (markerShape.equalsIgnoreCase("star")) {
            shapeFactory = new PointShapeFactory.Star(markerSize)
        } else if (markerShape.equalsIgnoreCase("Triangle")) {
            shapeFactory = new PointShapeFactory.Triangle(markerSize)
        } else if (markerShape.equalsIgnoreCase("X")) {
            shapeFactory = new PointShapeFactory.X(markerSize)
        } else /* if (markerShape.equalsIgnoreCase("square")) */ {
            shapeFactory = new PointShapeFactory.Square(markerSize)
        }

        ShapeWriter shapeWriter = new ShapeWriter({Coordinate mapCoordinate, Point2D shape ->
            double imageX = (1 - (bounds.maxX - mapCoordinate.x) / bounds.width) * imageWidth
            double imageY = ((bounds.maxY - mapCoordinate.y) / bounds.height) * imageHeight
            shape.setLocation(imageX,imageY);
        } as PointTransformation, shapeFactory)

        if (backgroundColor != null) {
            g2d.color = backgroundColor
            g2d.fillRect(0,0,size[0],size[1])    
        }

        Composite strokeComposite = g2d.getComposite()
        Composite fillComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)

        geometries.each{geometry->
            Shape shp = shapeWriter.toShape(geometry.g)
            // Fill
            if (!(geometry.g instanceof org.locationtech.jts.geom.Lineal)) {
                g2d.setComposite(fillComposite)
                g2d.setColor(fillColor)
                g2d.fill(shp)
            }
            // Stroke
            g2d.setComposite(strokeComposite)
            g2d.setStroke(new BasicStroke(strokeWidth))
            g2d.setColor(strokeColor)
            g2d.draw(shp)
            if (drawCoordinates) {
                g2d.setStroke(new BasicStroke(strokeWidth))
                java.util.List coords = geometry.coordinates
                if (coords.size() > 1) {
                    coords.each{c ->
                        Shape coordinateShp = shapeWriter.toShape(geometry.g.getFactory().createPoint(c))
                        // Fill
                        g2d.setComposite(fillComposite)
                        g2d.setColor(fillColor)
                        g2d.fill(coordinateShp)
                        // Stroke
                        g2d.setComposite(strokeComposite)
                        g2d.setColor(strokeColor)
                        g2d.draw(coordinateShp)
                    }
                }
            }
        }
    }

    /**
     * The JPanel used to render Geometry
     */
    private static class Panel extends JPanel {

        /**
         * The List of Geometries to render
         */
        private java.util.List<Geometry> geoms

        /**
         * The Map of options
         */
        private Map options

        /**
         * Create a new Panel with the List of Geometries to render
         * @param geoms The List of Geometries to render
         */
        Panel(java.util.Map options = [:], java.util.List<Geometry> geoms) {
            super()
            // Geometries
            this.geoms = geoms
            // Options
            this.options = options
        }

        /**
         * Override the paintComponent method to render the Geometries
         * @param gr The Graphics context
         */
        void paintComponent(Graphics gr) {
            super.paintComponent(gr)
            Graphics2D g2d = (Graphics2D)gr
            Bounds bounds = new GeometryCollection(geoms).bounds
            bounds.expandBy(bounds.width * 0.10)
            Viewer.draw(options, g2d, geoms)
        }
    }
}

<|start_filename|>src/main/groovy/geoscript/geom/io/GeoPackageReader.groovy<|end_filename|>
package geoscript.geom.io

import org.locationtech.jts.geom.Envelope
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.proj.Projection
import org.geotools.geometry.jts.ReferencedEnvelope
import org.geotools.geopkg.geom.GeoPkgGeomReader

/**
 * Read GeoPackage encoded Geometries
 * @author <NAME>
 */
class GeoPackageReader implements Reader {

    /**
     * Read a Geometry from a String
     * @param str The String
     * @return A Geometry
     */
    Geometry read(String str) {
        read(str.decodeHex())
    }

    /**
     * Read a Geometry from a byte array
     * @param bytes The byte array
     * @return A Geometry
     */
    Geometry read(byte[] bytes) {
        GeoPkgGeomReader reader = new GeoPkgGeomReader(bytes)
        Geometry.wrap(reader.get())
    }

    /**
     * Just read the Bounds from the hex string
     * @param str The hex string
     * @return The Bounds
     */
    Bounds readBounds(String str) {
        readBounds(str.decodeHex())
    }

    /**
     * Just read the Bounds from the byte arrya
     * @param byte[] The byte array
     * @return The Bounds
     */
    Bounds readBounds(byte[] bytes) {
        GeoPkgGeomReader reader = new GeoPkgGeomReader(bytes)
        Envelope env = reader.envelope
        Projection proj = null
        int srid = reader.header.srid
        if (srid > 0) {
            try {
                proj =  new Projection("EPSG:${reader.header.srid}")
            } catch(Exception e) {
                // Do nothing
            }
        }
        new Bounds(env.minX, env.minY, env.maxX, env.maxY, proj)
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/OverviewMapItem.groovy<|end_filename|>
package geoscript.carto

import geoscript.render.Map
import geoscript.style.Fill
import geoscript.style.Stroke
import geoscript.style.Style

/**
 * Add an overview map to a carto document
 * @author <NAME>
 */
class OverviewMapItem extends Item {

    Map linkedMap

    Map overviewMap

    Style areaStyle = new Fill("red", 0.2) + new Stroke("red", 1)

    boolean zoomIntoBounds = false

    double scaleFactor = 2.0

    /**
     * Create a Overview Map from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    OverviewMapItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the linked Map
     * @param map The linked Map
     * @return This OverviewMapItem
     */
    OverviewMapItem linkedMap(Map map) {
        this.linkedMap = map
        this
    }

    /**
     * Set the overview Map
     * @param map The overview Map
     * @return This OverviewMapItem
     */
    OverviewMapItem overviewMap(Map map) {
        this.overviewMap = map
        this
    }

    /**
     * Set the area Style
     * @param style The area Style
     * @return This OverviewMapItem
     */
    OverviewMapItem areaStyle(Style style) {
        this.areaStyle = style
        this
    }

    /**
     * Set whether to zoom into the bounds of the linked Map or not
     * @param zoomIntoBounds Whether to zoom into the bounds of the linked Map or not
     * @return This OverviewMap
     */
    OverviewMapItem zoomIntoBounds(boolean zoomIntoBounds) {
        this.zoomIntoBounds = zoomIntoBounds
        this
    }

    /**
     * Set the scale factor for expanding the linked Map bounds
     * @param scaleFactor The scale factor
     * @return This OverviewMap
     */
    OverviewMapItem scaleFactor(double scaleFactor) {
        this.scaleFactor = scaleFactor
        this
    }

    @Override
    String toString() {
        "OverviewMapItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, " +
                "linkedMap = ${linkedMap}, overViewMap = ${overviewMap}, areaStyle = ${areaStyle}, " +
                "zoomIntoBounds = ${zoomIntoBounds}, scaleFactor = ${scaleFactor})"
    }
}


<|start_filename|>src/main/groovy/geoscript/geom/io/TWkbReader.groovy<|end_filename|>
package geoscript.geom.io

import geoscript.geom.Geometry
import org.geotools.data.postgis.TWKBReader
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.io.WKBReader

/**
 * Read a {@link geoscript.geom.Geometry Geometry} from a TWKB hex String or byte array.
 * <p><blockquote><pre>
 * TWkbReader reader = new TWkbReader()
 * {@link geoscript.geom.Point Point} pt = reader.read("01000204")
 *
 * POINT (1 2)
 * </pre></blockquote></p>
 * @author <NAME>
 */
class TWkbReader implements Reader {

    /**
     * The GeoTools TWKBReader
     */
    private final static TWKBReader reader = new TWKBReader(new GeometryFactory())

    /**
     * Read a Geometry from a String
     * @param str The String
     * @return A Geometry
     */
    Geometry read(String str) {
        read(WKBReader.hexToBytes(str))
    }

    /**
     * Read a Geometry from a byte array
     * @param bytes The byte array
     * @return A Geometry
     */
    Geometry read(byte[] bytes) {
        Geometry.wrap(reader.read(bytes))
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/CartoFactories.groovy<|end_filename|>
package geoscript.carto

/**
 * Get registered CartoFactories
 * @author <NAME>
 */
class CartoFactories {

    static List<CartoFactory> list() {
        ServiceLoader.load(CartoFactory).iterator().collect()
    }

    static CartoFactory findByName(String name) {
        list().find{ CartoFactory cartoBuilderFactory ->
            cartoBuilderFactory.name.equalsIgnoreCase(name)
        }
    }

    static CartoFactory findByMimeType(String mimeType) {
        list().find{ CartoFactory cartoBuilderFactory ->
            cartoBuilderFactory.mimeType.equalsIgnoreCase(mimeType)
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/io/PbfTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.geom.Point
import geoscript.layer.Layer
import geoscript.layer.Pyramid
import geoscript.layer.Shapefile
import geoscript.layer.Tile
import geoscript.workspace.Memory
import geoscript.workspace.Workspace
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.*

/**
 * The Pbf Unit Test
 * @author <NAME>
 */
class PbfTest {

    @Test void read() {
        URL url = getClass().getClassLoader().getResource("pbf/1/1/0.pbf")

        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        Bounds bounds = pyramid.bounds(new Tile(1, 1, 0))

        List<Layer> layers = Pbf.read(url.bytes, bounds)
        assertTrue layers.size() > 0
    }

    @Test void writeRead() {
        URL url = getClass().getClassLoader().getResource("states.shp")
        Layer layer = new Shapefile(new File(url.toURI()))

        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        Bounds bounds = pyramid.bounds(new Tile(5, 5, 20))

        byte[] bytes = Pbf.write([layer], bounds)
        assertTrue bytes.length > 0

        Layer pbfLayer = Pbf.read(bytes, bounds)
        assertTrue pbfLayer.count > 0
    }

    @Test void test() {

        Workspace workspace = new Memory()
        Schema schema = new Schema("cities", [
                new Field("geom", "Point", "EPSG:4326"),
                new Field("id", "Integer"),
                new Field("name", "String")
        ])
        Layer layer = workspace.create(schema)
        layer.add([
                geom: new Point(-122.3204, 47.6024),
                id: 1,
                name: "Seattle"
        ])
        layer.add([
                geom: new Point(-122.48416, 47.2619),
                id: 2,
                name: "Tacoma"
        ])

        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid(origin: Pyramid.Origin.TOP_LEFT)
        Tile tile = new Tile(4,2,5)
        Bounds bounds = pyramid.bounds(tile)
        Bounds projectedBounds = bounds.reproject("EPSG:4326")
        Geometry projecteBoundsGeom = projectedBounds.geometry

        byte[] bytes = Pbf.write([layer], bounds)

        List<Layer> layers = Pbf.read(bytes, bounds)
        assertEquals(1, layers.size())
        assertEquals(2, layers[0].count)
    }

}


<|start_filename|>src/test/groovy/geoscript/style/UniqueValuesTest.groovy<|end_filename|>
package geoscript.style

import org.geotools.image.test.ImageAssert
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import javax.imageio.ImageIO

import static org.junit.jupiter.api.Assertions.*
import geoscript.filter.Color

import geoscript.render.Map
import geoscript.layer.Shapefile

/**
 * The UniqueValues UnitTest
 * @author <NAME>
 */
class UniqueValuesTest {

    @TempDir
    File folder

    @Test void create() {

        // Get states shapefile
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(file)

        // Create a Map
        Map map = new Map()
        map.addLayer(shapefile)

        // Default is random colors
        UniqueValues sym1 = new UniqueValues(shapefile, "STATE_ABBR")
        assertNotNull(sym1)
        assertEquals(49, sym1.parts.size())

        shapefile.style = sym1
        File imgFile = new File(folder,"uniquevalues_states1.png")
        map.render(imgFile)
        assertTrue imgFile.length() > 0

        // Color palette
        UniqueValues sym2 = new UniqueValues(shapefile, "STATE_ABBR", "Greens")
        assertNotNull(sym2)
        assertEquals(49, sym2.parts.size())

        shapefile.style = sym2
        imgFile = new File(folder,"uniquevalues_states2.png")
        map.render(imgFile)
        ImageAssert.assertEquals(getFile("geoscript/style/uniquevalues_states2.png"), ImageIO.read(imgFile), 200)

        // Color list
        UniqueValues sym3 = new UniqueValues(shapefile, "STATE_ABBR", ["teal","slateblue","tan","wheat","salmon"])
        assertNotNull(sym3)
        assertEquals(49, sym3.parts.size())

        shapefile.style = sym3
        imgFile = new File(folder,"uniquevalues_states3.png")
        map.render(imgFile)
        ImageAssert.assertEquals(getFile("geoscript/style/uniquevalues_states3.png"), ImageIO.read(imgFile), 300)

        // Color Closure
        UniqueValues sym4 = new UniqueValues(shapefile, "STATE_ABBR", {i,v -> Color.getRandom()})
        assertNotNull(sym4)
        assertEquals(49, sym4.parts.size())

        shapefile.style = sym4
        imgFile = new File(folder,"uniquevalues_states4.png")
        map.render(imgFile)
        assertTrue imgFile.length() > 0
    }

    private File getFile(String resource) {
        new File(getClass().getClassLoader().getResource(resource).toURI())
    }
}


<|start_filename|>src/test/groovy/geoscript/carto/SvgCartoFactoryTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue

class SvgCartoFactoryTest {

    @Test
    void getName() {
        assertEquals("svg", new SvgCartoFactory().getName())
    }

    @Test
    void getMimeType() {
        assertEquals("application/svg", new SvgCartoFactory().getMimeType())
    }

    @Test
    void create() {
        SvgCartoFactory cartoFactory = new SvgCartoFactory()
        CartoBuilder cartoBuilder = cartoFactory.create(PageSize.LETTER_LANDSCAPE)
        assertTrue(cartoBuilder instanceof SvgCartoBuilder)
    }
}


<|start_filename|>src/main/groovy/geoscript/style/DatabaseStyleRepository.groovy<|end_filename|>
package geoscript.style

import groovy.sql.GroovyRowResult
import groovy.sql.Sql

/**
 * A StyleRepository that stores styles in a database table called layer_styles.
 * H2, Postgres and SQLite are supported.
 * @author <NAME>
 */
class DatabaseStyleRepository implements StyleRepository {

    private final Sql sql

    private final Dialect dialect = Dialect.H2

    private enum Dialect {
        H2, POSTGRES, SQLITE
    }

    private DatabaseStyleRepository(Dialect dialect, Sql sql) {
        this.dialect = dialect
        this.sql = sql
        this.createTable()
    }

    static DatabaseStyleRepository forPostgres(Sql sql) {
        new DatabaseStyleRepository(Dialect.POSTGRES, sql)
    }

    static DatabaseStyleRepository forH2(Sql sql) {
        new DatabaseStyleRepository(Dialect.H2, sql)
    }

    static DatabaseStyleRepository forSqlite(Sql sql) {
        new DatabaseStyleRepository(Dialect.SQLITE, sql)
    }

    @Override
    String getDefaultForLayer(String layerName) {
        getForLayer(layerName, layerName)
    }

    @Override
    String getForLayer(String layerName, String styleName) {
        List results = sql.rows(
            "SELECT stylesld FROM layer_styles WHERE f_table_name = :name AND styleName = :styleName",
            [name: layerName, styleName: styleName]
        )
        if (results) {
            getText(results[0].stylesld)
        } else {
            null
        }
    }

    @Override
    List<Map<String, String>> getForLayer(String layerName) {
        sql.rows(
            """SELECT 
                    id,
                    f_table_catalog,
                    f_table_schema,
                    f_table_name,
                    f_geometry_column,
                    styleName,
                    styleQML,
                    styleSLD,
                    useAsDefault,
                    description,
                    owner,
                    ui,
                    update_time 
                FROM layer_styles WHERE f_table_name = :name""",
            [name: layerName]
        ).collect { GroovyRowResult result ->
            mapRow(result)
        }
    }

    @Override
    List<Map<String, String>> getAll() {
        sql.rows(
        """SELECT
                id,
                f_table_catalog,
                f_table_schema,
                f_table_name,
                f_geometry_column,
                styleName,
                styleQML,
                styleSLD,
                useAsDefault,
                description,
                owner,
                ui,
                update_time 
            FROM layer_styles"""
        ).collect { GroovyRowResult result ->
            mapRow(result)
        }
    }

    @Override
    void save(String layerName, String styleName, String style, Map options = [:]) {
        sql.execute("""INSERT INTO layer_styles (       
                f_table_catalog, 
                f_table_schema, 
                f_table_name, 
                f_geometry_column, 
                styleName, 
                styleSLD,
                styleQML,
                useAsDefault, 
                description, 
                owner,
                ui
            ) VALUES (:catalog, :schema, :table, :geometryColumn, :styleName, :sld, :qml, :default, :description, :owner, :ui)""", [
                catalog: options.get("catalog", "") as String,
                schema: options.get("schema", "public") as String,
                table: options.get("table", layerName) as String,
                geometryColumn: options.get("geometryColumn", "") as String,
                styleName: options.get("stylename", styleName) as String,
                sld: options.get("sld", style),
                qml: options.get("qml", options.get("qml","")),
                default: options.get("useAsDefault", layerName.equalsIgnoreCase(styleName)),
                description: options.get("description", "Style ${styleName} for Layer ${layerName}") as String,
                owner: options.get("owner", System.getProperty("user.name")) as String,
                ui: options.get("ui", options.get("ui","")),
        ])
    }

    @Override
    void delete(String layerName, String styleName) {
        sql.execute(
            "delete from layer_styles where f_table_name = :layerName AND stylename = :styleName",
            [layerName: layerName, styleName: styleName]
        )
    }

    private void createTable() {
        sql.execute('''CREATE TABLE IF NOT EXISTS layer_styles ( 
            id  ''' + getPrimaryKey() + ''',
            f_table_catalog VARCHAR(256),
            f_table_schema VARCHAR(256),
            f_table_name VARCHAR(256),
            f_geometry_column VARCHAR(256),
            styleName VARCHAR(30),
            styleQML TEXT,
            styleSLD TEXT,
            useAsDefault BOOLEAN,
            description VARCHAR,
            owner VARCHAR(30),
            ui VARCHAR(30),
            update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )''')
    }

    private Map mapRow(GroovyRowResult result) {
        [
            layerName: result.f_table_name,
            styleName: result.stylename,
            style: getText(result.stylesld),
            id: result.id,
            f_table_catalog: result.f_table_catalog,
            f_table_schema: result.f_table_schema,
            f_table_name: result.f_table_name,
            f_geometry_column: result.f_geometry_column,
            styleQML: getText(result.styleQML),
            styleSLD: getText(result.styleSLD),
            useAsDefault: result.useAsDefault,
            description: result.description,
            owner: result.owner,
            ui: result.ui,
            update_time: result.update_time
        ]
    }

    private String getPrimaryKey() {
        if (dialect == Dialect.H2) {
            "INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL"
        } else if (dialect == Dialect.POSTGRES) {
            "SERIAL PRIMARY KEY"
        } else if (dialect == Dialect.SQLITE) {
            "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL"
        } else {
            "INTEGER PRIMARY KEY AUTO INCREMENT NOT NULL"
        }
    }

    private String getText(Object result) {
        if (dialect == Dialect.H2) {
            result.characterStream.text
        } else if (dialect == Dialect.POSTGRES) {
            result
        } else if (dialect == Dialect.SQLITE) {
            result.toString()
        } else {
            null
        }
    }

}


<|start_filename|>src/main/groovy/geoscript/render/io/JsonMapReader.groovy<|end_filename|>
package geoscript.render.io

import geoscript.geom.Bounds
import geoscript.layer.Renderables
import geoscript.render.Map as GMap
import groovy.json.JsonSlurper

/**
 * Read a Map from a JSON String.
 * <pre>
 * {
 *    "width": 400,
 *    "height": 400,
 *    "type": "png",
 *    "backgroundColor": "blue",
 *    "proj": "EPSG:4326",
 *    "bounds": {
 *        "minX": -135.911779,
 *        "minY": 36.993573,
 *        "maxX": -96.536779,
 *        "maxY": 51.405899
 *    },
 *    "layers": [
 *        {"layertype": "layer", "file": "states.shp"}
 *    ]
 * }
 * </pre>
 * @author <NAME>
 */
class JsonMapReader implements MapReader {

    @Override
    String getName() {
        "json"
    }

    @Override
    GMap read(String str) {
        JsonSlurper jsonSlurper = new JsonSlurper()
        Map values = jsonSlurper.parseText(str)
        read(values)
    }

    /**
     * Read a GeoScript Map from a map of values
     * @param values A Map of values
     * @return A GeoScript Map
     */
    GMap read(Map values) {
        GMap map = new GMap(
                width: values.get("width", 600),
                height: values.get("height", 400),
                type: values.get("type", "png"),
                backgroundColor: values.get("backgroundColor"),
                fixAspectRatio: values.get("fixAspectRation", true),
                layers: Renderables.getRenderables(values.get("layers"))
        )
        if (values.get("proj")) {
            map.proj = values.get("proj")
        }
        if (values.get("bounds")) {
            Map bounds = values.get("bounds")
            map.bounds = new Bounds(bounds.minX, bounds.minY, bounds.maxX, bounds.maxY, bounds?.proj)
        }
        map
    }

}


<|start_filename|>src/main/groovy/geoscript/layer/io/KmlReader.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.feature.Feature
import geoscript.feature.Schema
import geoscript.layer.Layer
import geoscript.proj.Projection
import geoscript.workspace.Workspace
import geoscript.workspace.Memory
import org.geotools.xsd.Parser
import org.geotools.kml.KMLConfiguration
import org.opengis.feature.simple.SimpleFeature

/**
 * Read a GeoScript {@link geoscript.layer.Layer Layer} from a KML InputStream, File, or String.
 * @author <NAME>
 */
class KmlReader implements Reader {

    /**
     * The default Projection is EPSG:4326
     */
    private Projection defaultProj = new Projection("EPSG:4326")

    /**
     * Read a GeoScript Layer from a KML InputStream
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to EPSG:4326)</li>
     *     <li>name: The name of the Layer (defaults to kml)</li>
     * </ul>
     * @param input A KML InputStream
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], InputStream input) {
        // Default parameters
        Workspace workspace = options.get("workspace", new Memory())
        Projection proj = options.get("projection", defaultProj)
        String name = options.get("name", "kml")
        List subFields = options.get("subFields",["Geometry","name","description"])
        // Parse the KML
        Parser parser = new Parser(new KMLConfiguration())
        SimpleFeature f = parser.parse(input);
        // Collect placemarks
        List<SimpleFeature> placemarks = []
        collectPlaceMarks(placemarks, f);
        // Create the Schema and Layer
        Schema schema = new Schema(placemarks.size() > 0 ? placemarks[0].featureType : f.featureType)
                .reproject(proj, name).includeFields(subFields, name)
        Layer layer = workspace.create(schema)
        // Add Features
        placemarks.each{SimpleFeature placemark ->
            layer.add(new Feature(placemark))
        }
        layer
    }

    /**
     * Read a GeoScript Layer from a KML File
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to EPSG:4326)</li>
     *     <li>name: The name of the Layer (defaults to kml)</li>
     * </ul>
     * @param file A KML File
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], File file) {
         read(options, new FileInputStream(file))
    }

    /**
     * Read a GeoScript Layer from a String
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to EPSG:4326)</li>
     *     <li>name: The name of the Layer (defaults to kml)</li>
     * </ul>
     * @param str A KML String
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], String str) {
        read(options, new ByteArrayInputStream(str.getBytes("UTF-8")))
    }

    private void collectPlaceMarks(List<SimpleFeature> features, SimpleFeature feature) {
        if (feature.getAttribute("Feature")) {
            feature.getAttribute("Feature").each { f ->
                collectPlaceMarks(features, f)
            }
        } else {
            features.add(feature)
        }
    }

}


<|start_filename|>src/main/groovy/geoscript/geom/io/Gml2Writer.groovy<|end_filename|>
package geoscript.geom.io

import geoscript.geom.*
import org.locationtech.jts.geom.Coordinate

/**
 * Write a Geoscript {@link geoscript.geom.Geometry Geometry} to a GML Version 2 String.
 * <p><blockquote><pre>
 * Gml2Writer writer = new Gml2Writer()
 * String gml = writer.write(new {@link geoscript.geom.Point Point}(111,-47))
 *
 * &lt;gml:Point&gt;&lt;gml:coordinates&gt;111.0,-47.0&lt;/gml:coordinates&gt;&lt;/gml:Point&gt;
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Gml2Writer implements Writer {

    /**
     * Write the Geometry to GML
     * @param geom The Geometry
     * @return GML
     */
    String write(Geometry geom) {

        if (geom instanceof Point) {
            return "<gml:Point><gml:coordinates>${geom.x},${geom.y}</gml:coordinates></gml:Point>"
        }
        else if (geom instanceof LinearRing) {
            return "<gml:LinearRing><gml:coordinates>${getCoordinatesAsString(geom.coordinates)}</gml:coordinates></gml:LinearRing>"
        }
        else if (geom instanceof LineString) {
            return "<gml:LineString><gml:coordinates>${getCoordinatesAsString(geom.coordinates)}</gml:coordinates></gml:LineString>"
        }
        else if (geom instanceof Polygon) {
            return "<gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>${getCoordinatesAsString(geom.exteriorRing.coordinates)}</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs>${geom.interiorRings.collect{r-> "<gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>" + getCoordinatesAsString(r.coordinates) + "</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs>"}.join('')}</gml:Polygon>"
        }
        if (geom instanceof MultiPoint) {
            return "<gml:MultiPoint>${geom.geometries.collect{g->"<gml:pointMember>" + write(g) + "</gml:pointMember>"}.join('')}</gml:MultiPoint>"
        }
        else if (geom instanceof MultiLineString) {
            return """<gml:MultiLineString>${geom.geometries.collect{g->
                "<gml:lineStringMember><gml:LineString><gml:coordinates>${getCoordinatesAsString(g.coordinates)}</gml:coordinates></gml:LineString></gml:lineStringMember>"
                }.join('')}</gml:MultiLineString>"""
        }
        else if (geom instanceof MultiPolygon) {
            return """<gml:MultiPolygon>${geom.geometries.collect{g->
                "<gml:polygonMember>${write(g)}</gml:polygonMember>"
                }.join('')}</gml:MultiPolygon>"""
        }
        else {
            return "<gml:GeometryCollection>${geom.geometries.collect{g->"<gml:geometryMember>" + write(g) + "</gml:geometryMember>"}.join('')}</gml:GeometryCollection>"
        }
    }

    /**
     * Write an Array of Coordinates to a GML String
     * @param coords The Array of Coordinates
     * @return A GML String (x1,y1 x2,y2)
     */
    private String getCoordinatesAsString(Coordinate[] coords) {
        coords.collect{c -> "${c.x},${c.y}"}.join(" ")
    }

}



<|start_filename|>src/test/groovy/geoscript/workspace/MemoryTest.groovy<|end_filename|>
package geoscript.workspace

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.layer.*
import geoscript.feature.*
import geoscript.geom.Point

/**
 *
 */
class MemoryTest {

    @Test void create() {
        Memory mem = new Memory()
        Layer l = mem.create('widgets',[new Field("geom", "Point"), new Field("name", "String")])
        assertNotNull(l)
        l.add([new Point(1,1), "one"])
        l.add([new Point(2,2), "two"])
        l.add([new Point(3,3), "three"])
        assertEquals 3, l.count()
    }

    @Test void add() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shp = new Shapefile(file)

        Memory mem = new Memory()
        Layer l = mem.add(shp, 'counties')
        assertEquals shp.count(), l.count()
    }

    @Test void format() {
        Memory m = new Memory()
        assertEquals "Memory", m.format
    }

    @Test void remove() {
        Workspace workspace = new Memory()
        workspace.create("points",[new Field("geom","Point","EPSG:4326")])
        workspace.create("lines",[new Field("geom","LineString","EPSG:4326")])
        workspace.create("polygons",[new Field("geom","Polygon","EPSG:4326")])
        assertTrue workspace.has("points")
        assertTrue workspace.has("lines")
        assertTrue workspace.has("polygons")
        workspace.remove("points")
        workspace.remove(workspace.get("lines"))
        workspace.remove(workspace.get("polygons"))
        assertFalse workspace.has("points")
        assertFalse workspace.has("lines")
        assertFalse workspace.has("polygons")
    }
}



<|start_filename|>src/test/groovy/geoscript/layer/PbfVectorTileRendererTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.feature.Feature
import geoscript.geom.Bounds
import geoscript.layer.io.Pbf
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.*

/**
 * The PbfVectorTileRenderer Unit Test
 * @author <NAME>
 */
class PbfVectorTileRendererTest {

    @Test
    void render() {
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        Bounds bounds = pyramid.bounds(new Tile(5, 5, 20))
        PbfVectorTileRenderer renderer = new PbfVectorTileRenderer(shp, [shp.schema.get("STATE_NAME")])
        byte[] data = renderer.render(bounds)
        assertNotNull data
        assertTrue data.length > 0
        List layers = Pbf.read(data, bounds)
        assertEquals 1, layers.size()
        assertEquals 7, layers[0].count
        assertEquals "states", layers[0].name
        layers.each { Layer layer ->
            layer.eachFeature { Feature f ->
                assertNotNull f.geom
                assertNotNull f.get("STATE_NAME")
            }
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/index/GeoHashTest.groovy<|end_filename|>
package geoscript.index

import geoscript.geom.Bounds
import geoscript.geom.Point
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals

/**
 * The GeoHash Unit Test
 * @author <NAME>
 */
class GeoHashTest {

    @Test
    void encodeString() {
        GeoHash geohash = new GeoHash()
        assertEquals "ww8p1r4t8", geohash.encode(new Point(112.5584, 37.8324))
        assertEquals "wte", geohash.encode(new Point(117, 32), 3)
    }

    @Test
    void encodeLong() {
        GeoHash geohash = new GeoHash()
        assertEquals 4064984913515641, geohash.encodeLong(new Point(112.5584, 37.8324))
        assertEquals 63515389273681, geohash.encodeLong(new Point(112.5584, 37.8324), 46)
    }

    @Test
    void decodePointString() {
        GeoHash geohash = new GeoHash()
        assertEquals new Point(112.55838632583618, 37.83238649368286), geohash.decode("ww8p1r4t8")
    }

    @Test
    void decodePointLong() {
        GeoHash geohash = new GeoHash()
        assertEquals new Point(112.55839973688126, 37.83240124583244), geohash.decode(4064984913515641)
    }

    @Test
    void decodeBoundsString() {
        GeoHash geohash = new GeoHash()
        Bounds bounds = geohash.decodeBounds("ww8p1r4t8")
        assertEquals new Bounds(112.55836486816406, 37.83236503601074, 112.5584077835083, 37.83240795135498), bounds
    }

    @Test
    void decodeBoundsLong() {
        GeoHash geohash = new GeoHash()
        Bounds bounds = geohash.decodeBounds(4064984913515641)
        assertEquals new Bounds(112.55839705467224, 37.832399904727936, 112.55840241909027, 37.83240258693695), bounds
    }

    @Test
    void neighborString() {
        GeoHash geohash = new GeoHash()
        assertEquals "dqcjw", geohash.neighbor("dqcjq", GeoHash.Direction.NORTH)
        assertEquals "dqcjj", geohash.neighbor("DQCJQ", GeoHash.Direction.SOUTHWEST)
    }

    @Test
    void neighborLong() {
        GeoHash geohash = new GeoHash()
        assertEquals 1702789520, geohash.neighbor(1702789509, GeoHash.Direction.NORTH, 32)
        assertEquals 27898503327465, geohash.neighbor(27898503327470, GeoHash.Direction.SOUTHWEST, 46)
    }

    @Test
    void neighborsString() {
        GeoHash geohash = new GeoHash()
        Map neighbors = geohash.neighbors('dqcjq')
        assertEquals "dqcjw", neighbors[GeoHash.Direction.NORTH]
        assertEquals "dqcjx", neighbors[GeoHash.Direction.NORTHEAST]
        assertEquals "dqcjr", neighbors[GeoHash.Direction.EAST]
        assertEquals "dqcjp", neighbors[GeoHash.Direction.SOUTHEAST]
        assertEquals "dqcjn", neighbors[GeoHash.Direction.SOUTH]
        assertEquals "dqcjj", neighbors[GeoHash.Direction.SOUTHWEST]
        assertEquals "dqcjm", neighbors[GeoHash.Direction.WEST]
        assertEquals "dqcjt", neighbors[GeoHash.Direction.NORTHWEST]
    }

    @Test
    void neighborsLong() {
        GeoHash geohash = new GeoHash()
        Map neighbors = geohash.neighbors(1702789509, 32)
        assertEquals 1702789520, neighbors[GeoHash.Direction.NORTH]
        assertEquals 1702789522, neighbors[GeoHash.Direction.NORTHEAST]
        assertEquals 1702789511, neighbors[GeoHash.Direction.EAST]
        assertEquals 1702789510, neighbors[GeoHash.Direction.SOUTHEAST]
        assertEquals 1702789508, neighbors[GeoHash.Direction.SOUTH]
        assertEquals 1702789422, neighbors[GeoHash.Direction.SOUTHWEST]
        assertEquals 1702789423, neighbors[GeoHash.Direction.WEST]
        assertEquals 1702789434, neighbors[GeoHash.Direction.NORTHWEST]
    }

    @Test
    void bboxesString() {
        GeoHash geohash = new GeoHash()
        List bboxes = geohash.bboxes(new Bounds(120, 30, 120.0001, 30.0001), 8)
        assertEquals 2, bboxes.size()
        assertEquals "wtm6dtm6", bboxes[0]
        assertEquals "wtm6dtm7", bboxes[1]
    }

    @Test
    void bboxesLong() {
        GeoHash geohash = new GeoHash()
        List bboxes = geohash.bboxesLong(new Bounds(120, 30, 120.0001, 30.0001), 50)
        assertEquals 190, bboxes.size()
        assertEquals 1013309916158361, bboxes[0]
        assertEquals 1013309916159519, bboxes[189]
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/ImageCartoBuilder.groovy<|end_filename|>
package geoscript.carto

import javax.imageio.ImageIO
import java.awt.Graphics2D
import java.awt.RenderingHints
import java.awt.image.BufferedImage

/**
 * Build a cartographic document as an Image
 * @author <NAME>
 */
class ImageCartoBuilder implements CartoBuilder {

    private final Java2DCartoBuilder java2DCartoBuilder

    private final BufferedImage image

    private final ImageType imageType

    /**
     * Create a ImageCartoBuilder
     * @param pageSize The PageSize
     * @param imageType The ImageType
     */
    ImageCartoBuilder(PageSize pageSize, ImageType imageType) {
        this.imageType = imageType
        this.image = new BufferedImage(pageSize.width, pageSize.height, imageType.bufferedImageType)
        Graphics2D g = image.createGraphics()
        g.renderingHints = [
                (RenderingHints.KEY_ANTIALIASING)     : RenderingHints.VALUE_ANTIALIAS_ON,
                (RenderingHints.KEY_TEXT_ANTIALIASING): RenderingHints.VALUE_TEXT_ANTIALIAS_ON
        ]
        this.java2DCartoBuilder = new Java2DCartoBuilder(g, pageSize)
    }

    @Override
    CartoBuilder dateText(DateTextItem dateTextItem) {
        java2DCartoBuilder.dateText(dateTextItem)
        this
    }

    @Override
    CartoBuilder image(ImageItem imageItem) {
        java2DCartoBuilder.image(imageItem)
        this
    }

    @Override
    CartoBuilder line(LineItem lineItem) {
        java2DCartoBuilder.line(lineItem)
        this
    }

    @Override
    CartoBuilder map(MapItem mapItem) {
        java2DCartoBuilder.map(mapItem)
        this
    }

    @Override
    CartoBuilder northArrow(NorthArrowItem northArrowItem) {
        java2DCartoBuilder.northArrow(northArrowItem)
        this
    }

    @Override
    CartoBuilder text(TextItem textItem) {
        java2DCartoBuilder.text(textItem)
        this
    }

    @Override
    CartoBuilder paragraph(ParagraphItem paragraphItem) {
        java2DCartoBuilder.paragraph(paragraphItem)
        this
    }

    @Override
    CartoBuilder rectangle(RectangleItem rectangleItem) {
        java2DCartoBuilder.rectangle(rectangleItem)
        this
    }

    @Override
    CartoBuilder scaleText(ScaleTextItem scaleTextItem) {
        java2DCartoBuilder.scaleText(scaleTextItem)
        this
    }

    @Override
    CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {
        java2DCartoBuilder.scaleBar(scaleBarItem)
        this
    }

    @Override
    CartoBuilder grid(GridItem gridItem) {
        java2DCartoBuilder.grid(gridItem)
        this
    }

    @Override
    CartoBuilder overViewMap(OverviewMapItem overviewMapItem) {
        java2DCartoBuilder.overViewMap(overviewMapItem)
        this
    }

    @Override
    CartoBuilder table(TableItem tableItem) {
        java2DCartoBuilder.table(tableItem)
        this
    }

    @Override
    CartoBuilder legend(LegendItem legendItem) {
        java2DCartoBuilder.legend(legendItem)
        this
    }

    @Override
    void build(OutputStream outputStream) {
        java2DCartoBuilder.graphics.dispose()
        ImageIO.write(image, imageType.name, outputStream)
    }

    /**
     * Get the cartographic document as a BufferedImage
     * @return A BufferedImage
     */
    BufferedImage getImage() {
        image
    }

    enum ImageType {

        PNG ("PNG", "image/png", BufferedImage.TYPE_INT_ARGB),
        JPEG("JPEG", "image/jpeg", BufferedImage.TYPE_INT_RGB)

        String name
        String mimeType
        int bufferedImageType

        ImageType(String name, String mimeType, int imageType) {
            this.name = name
            this.mimeType = mimeType
            this.bufferedImageType = imageType
        }

    }

}


<|start_filename|>src/main/groovy/geoscript/render/Plot.groovy<|end_filename|>
package geoscript.render

import org.locationtech.jts.geom.Geometry as JtsGeometry

import geoscript.feature.Feature
import geoscript.geom.Geometry
import geoscript.layer.Layer
import java.awt.image.BufferedImage
import javax.swing.JFrame
import org.geotools.renderer.chart.GeometryDataset
import org.geotools.renderer.chart.GeometryRenderer
import org.jfree.chart.ChartPanel
import org.jfree.chart.ChartUtilities
import org.jfree.chart.JFreeChart
import org.jfree.chart.plot.XYPlot

/**
 * Plot a {@link geoscript.geom.Geometry Geometry}, {@link geoscript.feature.Feature Feature}, or {@link geoscript.layer.Layer Layer}.
 * <p><blockquote><pre>
 * import static geoscript.render.Plot.*
 * import geoscript.geom.Point
 *
 * plot(new Point(1,1).buffer(10.0))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Plot {

    /**
     * Plot a Geometry
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     *  <li>out = OutputStream or File to write to.  If null an interactive app is opened.</li>
     *  <li>type = The image type ("png", "jpeg")</li>
     * </ul>
     * @param geometry The Geometry
     */
    static void plot(java.util.Map options = [:], Geometry geometry) {
        plot(options, [geometry])
    }

    /**
     * Plot a List of Geometries
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     *  <li>out = OutputStream or File to write to.  If null an interactive app is opened.</li>
     *  <li>type = The image type ("png", "jpeg")</li>
     * </ul>
     * @param geometries The List of Geometries
     */
    static void plot(java.util.Map options = [:], List geometries) {
        if (geometries.size() > 0 && geometries[0] instanceof Feature) {
            geometries = geometries.collect {it.geom}
        }
        def plot = createPlot(options, geometries)
        List size = options.get("size", [500,500])
        String type = options.get("type","png")
        def out = options.get("out",null)
        if (out == null) {
            def panel = new ChartPanel(plot)
            def frame = new JFrame("GeoScript Geometry Plot")
            // If we are opening Windows from the GroovyConsole, we can't use EXIT_ON_CLOSE because the GroovyConsole
            // itself will exit
            if (java.awt.Frame.frames.find{it.title.contains("GroovyConsole")}) {
                frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
            } else {
                // The Groovy Shell has a special SecurityManager that doesn't allow EXIT_ON_CLOSE
                try { frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE } catch (SecurityException ex) {frame.defaultCloseOperation = JFrame.HIDE_ON_CLOSE}
            }
            frame.contentPane = panel
            frame.setSize(size[0] as int, size[1] as int)
            frame.resizable = false
            frame.visible = true
        } else if (out instanceof OutputStream) {
            String
            plotToOutputStream(plot, size, out, type)
        } else {
            File file = out instanceof File ? out : new File(out.toString())
            plotToFile(plot, size, file)
        }
    }

    /**
     * Plot a Feature
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     *  <li>out = OutputStream or File to write to.  If null an interactive app is opened.</li>
     *  <li>type = The image type ("png", "jpeg")</li>
     * </ul>
     * @param feature The Feature
     */
    static void plot(java.util.Map options = [:], Feature feature) {
        plot(options, feature.geom)
    }

    /**
     * Plot a Layer
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     *  <li>out = OutputStream or File to write to.  If null an interactive app is opened.</li>
     *  <li>type = The image type ("png", "jpeg")</li>
     * </ul>
     * @param layer The Layer
     */
    static void plot(java.util.Map options = [:], Layer layer) {
        plot(options, layer.features.collect {it.geom})
    }

    /**
     * Plot a Geometry to an Image
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     * </ul>
     * @param geometry The Geometry
     * @return A BufferedImage
     */
    static BufferedImage plotToImage(java.util.Map options = [:], Geometry geometry) {
        plotToImage(options, [geometry])
    }

    /**
     * Plot a List of Geometries to an Image
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     * </ul>
     * @param geometries The List of Geometries
     * @return A BufferedImage
     */
    static BufferedImage plotToImage(java.util.Map options = [:], List geometries) {
        if (geometries.size() > 0 && geometries[0] instanceof Feature) {
            geometries = geometries.collect {it.geom}
        }
        def chart = createPlot(options, geometries)
        List size = options.get("size", [500,500])
        chart.createBufferedImage(size[0] as int, size[1] as int)
    }

    /**
     * Plot a Feature to an Image
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     * </ul>
     * @param feature The Feature
     * @return A BufferedImage
     */
    static BufferedImage plotToImage(java.util.Map options = [:], Feature feature) {
        plotToImage(options, feature.geom)
    }

    /**
     * Plot a Layer to an Image
     * @param options A Map of options or named parameters
     * <ul>
     *  <li>size = The size of the plot ([500,500])</li>
     * </ul>
     * @param layer The Layer
     * @return A BufferedImage
     */
    static BufferedImage plotToImage(java.util.Map options = [:], Layer layer) {
        plotToImage(options, layer.features.collect {it.geom})
    }

    /**
     * Create a JFreeChart from a Geometry (or List of Geometries) using the
     * GeoTools Geometry Dataset
     * @param geom A Geometry (or List of Geometries)
     * @return The JFreeChart
     */
    private static JFreeChart createPlot(java.util.Map options = [:], List<Geometry> geom) {
        def dataset = new GeometryDataset(geom.collect {g -> g.g} as JtsGeometry[])
        def renderer = new GeometryRenderer()
        renderer.legend = options.get("legend", false)
        renderer.fillCoordinates = options.get("fillCoords", false)
        renderer.fillPolygons = options.get("fillPolys", false)
        renderer.renderCoordinates = options.get("drawCoords", true)
        def plot = new XYPlot(dataset, dataset.domain, dataset.range, renderer)
        new JFreeChart(plot)
    }

    /**
     * Plot the JFreeChart to the OutputStream
     * @param chart The JFreeChart
     * @param size The chart size
     * @param out The OutputStream
     * @param type The image type ("png" or "jpeg")
     */
    private static void plotToOutputStream(JFreeChart chart, List size, OutputStream out, String type) {
        if (type.equalsIgnoreCase("png")) {
            ChartUtilities.writeChartAsPNG(out, chart, size[0] as int, size[1] as int)
        }
        else if (type.equalsIgnoreCase("jpg") || type.equalsIgnoreCase("jpeg")) {
            ChartUtilities.writeChartAsJPEG(out, chart, size[0] as int, size[1] as int)
        }
        else {
            throw new IllegalArgumentException("Unsupported image format! Only PNGs and JPEGs are supported!")
        }
    }

    /**
     * Plot a Geometry (or List of Geometries) to an image File
     * @param geom The Geometry (or List of Geometries)
     * @param size The size of the image
     * @param file The File
     */
    private static void plotToFile(JFreeChart chart, List size = [500, 500], File file) {
        String fileName = file.absolutePath
        String imageFormat = fileName.substring(fileName.lastIndexOf(".") + 1)
        OutputStream out = new BufferedOutputStream(new FileOutputStream(file))
        plotToOutputStream(chart, size, out, imageFormat)
        out.flush()
        out.close()
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/io/GeobufWriterTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.geom.Point
import geoscript.layer.Layer
import geoscript.workspace.Memory
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The GeobufWriter Unit Test.
 * @author <NAME>
 */
class GeobufWriterTest {

    @Test void write() {
        Schema schema = new Schema("houses", [new Field("geom", "Point"), new Field("name", "string"), new Field("price", "float")])
        Memory memory = new Memory()
        Layer layer = memory.create(schema)
        layer.add(new Feature([new Point(111, -47), "House", 12.5], "house1", schema))
        layer.add(new Feature([new Point(121, -45), "School", 22.7], "house2", schema))

        GeobufWriter writer = new GeobufWriter()
        String hex = writer.write(layer)

        GeobufReader reader = new GeobufReader()
        Layer layerFromGeobuf = reader.read(hex)

        assertEquals 2, layerFromGeobuf.count
        assertEquals "POINT (111 -47)", layerFromGeobuf.features[0].geom.wkt
        assertEquals "House", layerFromGeobuf.features[0]["name"]
        assertEquals "12.5", layerFromGeobuf.features[0]["price"]
        assertEquals "POINT (121 -45)", layerFromGeobuf.features[1].geom.wkt
        assertEquals "School", layerFromGeobuf.features[1]["name"]
        assertEquals "22.7", layerFromGeobuf.features[1]["price"]

    }
}


<|start_filename|>src/test/groovy/geoscript/ServerTestUtil.groovy<|end_filename|>
package geoscript

import okhttp3.mockwebserver.MockWebServer
import okio.Buffer
import okio.Okio

class ServerTestUtil {

    static void withServer(Closure closure) {
        MockWebServer server = new MockWebServer()
        try {
            closure.call(server)
        } finally {
            server.shutdown()
        }
    }

    static File getResource(String resource) {
        new File(ServerTestUtil.getClassLoader().getResource(resource).toURI())
    }

    static Buffer fileToBytes(File file) throws IOException {
        Buffer result = new Buffer()
        result.writeAll(Okio.source(file))
        result
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/PngCartoFactory.groovy<|end_filename|>
package geoscript.carto

/**
 * A CartoFactory that creates a CartoBuilder that produces PNG images.
 * @author <NAME>
 */
class PngCartoFactory implements CartoFactory {

    @Override
    String getName() {
        "png"
    }

    @Override
    String getMimeType() {
        "image/png"
    }

    @Override
    CartoBuilder create(PageSize pageSize) {
        new ImageCartoBuilder(pageSize, ImageCartoBuilder.ImageType.PNG)
    }

}


<|start_filename|>src/main/groovy/geoscript/geom/Bounds.groovy<|end_filename|>
package geoscript.geom

import org.geotools.geometry.jts.ReferencedEnvelope
import org.locationtech.jts.geom.Envelope
import geoscript.proj.Projection
import org.locationtech.jts.util.GeometricShapeFactory
import org.locationtech.jts.geom.util.SineStarFactory

/**
 * A Bounds is an Envelope with a {@link geoscript.proj.Projection Projection}.
 * <p><blockquote><pre>
 * Bounds b = new Bounds(1,2,3,4, new {@link geoscript.proj.Projection Projection}("EPSG:2927"))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Bounds {
	
    /**
     * The GeoTools' wrapped ReferencedEnvelope
     */
    ReferencedEnvelope env
	
    /**
     * Create a new Bounds wrapping a ReferencedEnvelope.
     * <p><code>ReferencedEnvelope e = new ReferencedEnvelope(1,3,2,4,null)</code></p>
     * <p><code>Bounds b = new Bounds(e)</code></p>
     * @param env The ReferencedEnvelope
     */
    Bounds(ReferencedEnvelope env) {
        this.env = env
    }

    /**
     * Create a new Bounds wrapping an Envelope.
     * <p><code>Envelope e = new Envelope(1,3,2,4)</code></p>
     * <p><code>Bounds b = new Bounds(e)</code></p>
     * @param env The ReferencedEnvelope
     */
    Bounds(Envelope env) {
        this(new ReferencedEnvelope(env, null))
    }

    /**
     * Create a new Bounds with minX, minY, maxX, and maxY coordinates.
     * <p><code>Bounds b = new Bounds(1,2,3,4)</code></p>
     * @param minX The left/west most coordinate (minX)
     * @param minY the bottom/south most coordinate (minY)
     * @param maxX The right/east most coordinate (maxX)
     * @param maxY The top/north most coordinate (maxY)
     */
    Bounds(double minX, double minY, double maxX, double maxY) {
        this(minX, minY, maxX, maxY, null)
    }
	
    /**
     * Create a new Bounds with minX, minY, maxX, and maxY coordinates
     * and a Projection.
     * <p><code>Bounds b = new Bounds(1,2,3,4, new Projection("EPSG:2927"))</code></p>
     * <p><code>Bounds b = new Bounds(1,2,3,4, "EPSG:2927")</code></p>
     * @param minX The left/minX most coordinate (minX)
     * @param minY the bottom/minY most coordinate (minY)
     * @param maxX The right/maxX most coordinate (maxX)
     * @param maxY The top/maxY most coordinate (maxY)
     * @param proj The Projection can either be a Projection or a String
     */
    Bounds(double minX, double minY, double maxX, double maxY, def proj) {
        this(new ReferencedEnvelope(minX, maxX, minY, maxY, new Projection(proj).crs))
    }

    /**
     * Create a new Bounds from a Point (which can either be the origin/lower left or center) and a width and height
     * @param point The Point origin or center
     * @param width The width
     * @param height The height
     * @param isOrigin Whether the Point is the origin (true) or the center (false)
     */
    Bounds(Point point, double width, double height, boolean isOrigin = true) {
        this(createBounds(point, width, height, isOrigin).env)
    }

    /**
     * Create a new Bounds from a Point (which can either be the origin/lower left or center) and a width and height
     * @param point The Point origin or center
     * @param width The width
     * @param height The height
     * @param isOrigin Whether the Point is the origin (true) or the center (false)
     */
    private static Bounds createBounds(Point point, double width, double height, boolean isOrigin = true) {
        // Lower left
        if (isOrigin) {
            return new Bounds(point.x, point.y, point.x + width, point.y + height)
        }
        // Center
        else {
            return new Bounds(point.x - width / 2, point.y - height / 2, point.x + width / 2, point.y + height / 2)
        }
    }

    /**
     * Create a Bounds from a String or return null
     * @param str The string
     * @return A Bounds or null
     */
    static Bounds fromString(String str) {
        Bounds bounds = null
        if (str && str.trim()) {
            def parts = str.split(",")
            if (parts.length >= 4) {
                bounds = new Bounds(parts[0] as double, parts[1] as double, parts[2] as double, parts[3] as double,
                        parts.length == 5 ? new Projection(parts[4]) : null)
            }
            if (!bounds) {
                parts = str.split(" ")
                if (parts.length >= 4) {
                    bounds = new Bounds(parts[0] as double, parts[1] as double, parts[2] as double, parts[3] as double,
                            parts.length == 5 ? new Projection(parts[4]) : null)
                }
            }
        }
        bounds
    }

    /**
     * Get the left/west most coordinate (minX)
     * @return The left/west most coordinate (minX)
     */
    double getMinX() {
        env.minX
    }

    /**
     * Get the right/east most coordinate (maxX)
     * @return The right/east most coordinate (maxX)
     */
    double getMaxX() {
        env.maxX
    }

    /**
     * Get the bottom/south most coordinate (minY)
     * @return The bottom/south most coordinate (minY)
     */
    double getMinY() {
        env.minY
    }

    /**
     * Get the top/north most coordinate (maxY)
     * @return The top/north most coordinate (maxY)
     */
    double getMaxY() {
        env.maxY
    }

    /**
     * Get the area of this Bounds
     * @return The area
     */
    double getArea() {
        env.area
    }

    /**
     * Get the width
     * @return The width
     */
    double getWidth() {
        env.width
    }

    /**
     * Get the height
     * @return The height
     */
    double getHeight() {
        env.height
    }

    /**
     * Get the ratio of width to height for this bounds.
     * @return The aspect ratio
     */
    double getAspect() {
        width / height
    }

    /**
     * Expand the Bounds by the given distance in all directions
     * @param distance The distance
     * @return The modified Bounds
     */
    Bounds expandBy(double distance) {
        env.expandBy(distance)
        this
    }

    /**
     * Expand this Bounds to include another Bounds
     * @param other Another Bounds
     * @return The modified Bounds
     */
    Bounds expand(Bounds other) {
        ReferencedEnvelope otherEnv
        if (this.proj.equals(other.proj)) {
            otherEnv = other.env
        } else {
            otherEnv = other.reproject(this.proj).env
        }
        if (otherEnv.contains(this.env)) {
            this.env = otherEnv
        } else if (!this.env.contains(otherEnv)) {
            env.expandToInclude(otherEnv)
        }
        this
    }

    /**
     * Scales the current Bounds by a specific factor.
     * @param factor The scale factor
     * @return The new scaled Bounds
     */
    Bounds scale(double factor) {
        double w = width * (factor - 1) / 2
        double h = height * (factor - 1) / 2
        new Bounds(minX - w, minY - h, maxX + w, maxY + h)
    }


    /**
     * Get the Projection (if any) or null
     * @return The Projection (if any) or null
     */
    Projection getProj() {
        if (env.coordinateReferenceSystem)
            return new Projection(env.coordinateReferenceSystem)
        else
            return null
    }

    /**
     * Set a Projection if the current Projection is not set
     * or reproject if needed
     * @param projection The new Projection
     */
    void setProj(def projection) {
        def p = new Projection(projection)
        if (!getProj()) {
            this.env = new Bounds(minX, minY, maxX, maxY, p).env
        } else if (!p.equals(getProj())) {
            this.env = reproject(p).env
        }
    }

    /**
     * Reprojects the Bounds
     * @param proj A Projection or String
     * @return The reprojected Bounds
     */
    Bounds reproject(def proj) {
        proj = new Projection(proj)
        new Bounds(env.transform(proj.crs, true))
    }

    /**
     * Convert this Bounds into a Geometry object
     * @return A Geometry
     */
    Geometry getGeometry() {
        Geometry.wrap(Geometry.factory.toGeometry(env))
    }

    /**
     * Convert this Bounds into a Polygon
     * @return A Polygon
     */
    Polygon getPolygon() {
        new Polygon([minX, minY], [minX, maxY], [maxX, maxY], [maxX, minY], [minX, minY])
    }

    /**
     * Get the corners of the Bounds as a List of Points.  The ordering is:
     * [minX,minY],[minX, maxY],[maxX,maxY],[maxX,minY]
     * @return A List of Points
     */
    List getCorners() {
        [
                new Point(minX, minY),
                new Point(minX, maxY),
                new Point(maxX, maxY),
                new Point(maxX, minY)
        ]
    }

    /**
     * Partitions the bounding box into a set of smaller bounding boxes.
     * @param res The resolution to tile at and should be in range 0-1.
     * @return A List of smaller bounding boxes
     */
    List<Bounds> tile(double res) {
        double dx = width * res
        double dy = height * res
        List bounds = []
        double y = minY
        while (y < maxY) {
            double x = minX
            while (x < maxX) {
                bounds.add(new Bounds(x,y,Math.min(x + dx, maxX), Math.min(y+dy, maxY), proj))
                x += dx
            }
            y += dy
        }
        bounds
    }

    /**
     * Calculate a quad tree for this Bounds between the start and stop levels. The Closure
     * is called for each new Bounds generated.
     * @param start The start level
     * @param stop The stop level
     * @param closure The Closure called for each new Bounds
     */
    void quadTree(int start, int stop, Closure closure) {
        Projection p = getProj()
        for(int level = start; level < stop; level++) {
            double factor = Math.pow(2, level)
            double dx = (this.maxX - this.minX) / factor
            double dy = (this.maxY - this.minY) / factor
            double minx = this.minX
            for(int x = 0; x < factor; ++x) {
                double miny = this.minY
                for(int y = 0; y < factor; ++y) {
                    closure(new Bounds(minx, miny, minx + dx, miny + dy, p))
                    miny += dy
                }
                minx += dx
            }
        }
    }

    /**
     * Get whether the Bounds is empty (width and height are zero)
     * @return Whether the Bounds is empty
     */
    boolean isEmpty() {
       env.empty || area == 0.0
    }
    
    /**
     * Determine whether this Bounds equals another Bounds
     * @param other The other Bounds
     * @return Whether this Bounds and the other Bounds are equal
     */
    @Override
    boolean equals(Object other) {
        other instanceof Bounds && env.equals(other.env)
    }

    /**
     * Get the hash code for this Bounds
     * @return The hash code for this Bounds
     */
    @Override
    int hashCode() {
        env.hashCode()
    }

    /**
     * Determine whether this Bounds contains the other Bounds
     * @param other The other Bounds
     * @return Whether this Bounds contains the other Bounds
     */
    boolean contains(Bounds other) {
        env.contains(other.env)
    }

    /**
     * Determine whether this Bounds contains the Point
     * @param point The Point
     * @return Whether this Bounds contains the Point
     */
    boolean contains(Point point) {
        env.contains(point.g.coordinate)
    }
    
    /**
     * Determine whether this Bounds intersects with the other Bounds
     * @param other The other Bounds
     * @return Whether this Bounds intersects with the other Bounds
     */
    boolean intersects(Bounds other) {
        env.intersects(other.env)
    }
    
    /**
     * Calculate the intersection between this Bounds and the other Bounds
     * @param other The other Bounds
     * @return The intersection Bounds between this and the other Bounds
     */
    Bounds intersection(Bounds other) {
        new Bounds(new ReferencedEnvelope(env.intersection(other.env), this.proj?.crs))
    }

    /**
     * Ensure that the Bounds has a width and height.  Handle vertical and horizontal lines and points.
     * @return A new Bounds with a width and height
     */
    Bounds ensureWidthAndHeight() {
        Bounds b = new Bounds(env)
        if (b.width == 0 || b.height == 0) {
            if (b.height > 0) {
                double h = b.height / 2.0
                b = new Bounds(b.minX - h, b.minY, b.maxX + h, b.maxY, b.proj)
            } else if (b.width > 0) {
                double w = b.width / 2.0
                b = new Bounds(b.minX, b.minY - w, b.maxX, b.maxY + w, b.proj)
            } else {
                def e = new Point(b.minX, b.minY).buffer(0.1).envelopeInternal
                b = new Bounds(e.minX, e.minY, e.maxX, e.maxY, proj)
            }
        }
        return b
    }

    /**
     * Fix the aspect ration of this Bounds based on the given width and height.
     * @param w The image width
     * @param h The image height
     * @return A new Bounds
     */
    Bounds fixAspectRatio(int w, int h) {
        double mapWidth = this.width
        double mapHeight = this.height
        double scaleX = w / mapWidth
        double scaleY = h / mapHeight
        double scale
        if (scaleX < scaleY) {
            scale = scaleX
        } else {
            scale = scaleY
        }
        double deltaX = w / scale - mapWidth
        double deltaY = h / scale - mapHeight
        double minX = this.minX - deltaX / 2D
        double maxX = this.maxX + deltaX / 2D
        double minY = this.minY - deltaY / 2D
        double maxY = this.maxY + deltaY / 2D
        new Bounds(minX, minY, maxX, maxY, this.proj)
    }


    /**
     * Get a value from this Bounds at the given index (minX = 0, minY = 1,
     * maxX = 2, maxY = 3).
     * <p><code>Bounds b = new Bounds(1,2,3,4)</code></p>
     * <p><code>def w = b[0]</code></p>
     * <p><code>def (w,s,e,n) = b</code></p>
     * @return A value from this Bounds or null if the index is greater than 3
     */
    Object getAt(int index) {
        if (index == 0) {
            minX
        } else if (index == 1) {
            minY
        } else if (index == 2) {
            maxX
        } else if (index == 3) {
            maxY
        } else {
            null
        }
    }

    /**
     * Get a generated grid with the given number of the rows and column
     * @param columns The number of columns
     * @param rows The number of rows
     * @param type The cell type (polygon, point, circle/ellipse, hexagon, hexagon-inv, triangle)
     * @return The generated grid as a Geometry
     */
    Geometry getGrid(int columns, int rows, String type = "polygon") {
        List geoms = []
        this.generateGrid(rows, columns, type, {cell, c, r ->
            geoms.add(cell)
        })
        new GeometryCollection(geoms)
    }

    /**
     * Generate a grid with the given number or rows and columns
     * @param columns The number of columns
     * @param rows The number of rows
     * @param type The cell type (polygon, point, circle/ellipse, hexagon, hexagon-inv, triangle)
     * @param c A Closure that is called with each Geometry cell with the geometry, the column, and the row
     */
    void generateGrid(int columns, int rows, String type, Closure c) {
        double cellWidth = this.width / columns
        double cellHeight = this.height / rows
        double x = this.minX
        double y = this.minY
        (1..columns).each {column ->
            (1..rows).each {row ->
                Bounds b = new Bounds(x,y,x+cellWidth,y+cellHeight)
                Geometry g = b.geometry
                if (type.equalsIgnoreCase("point")) {
                    g = g.centroid
                } else if (type.equalsIgnoreCase("ellipse") || type.equalsIgnoreCase("circle")) {
                    g = b.createEllipse(100)
                } else if (type.equalsIgnoreCase("hexagon")) {
                    Bounds newBounds = new Bounds(
                            b.minX - b.width / 6,
                            column % 2 == 0 ? b.minY : b.minY - b.height / 2,
                            b.maxX + b.width / 6,
                            column % 2 == 0 ? b.maxY : b.maxY - b.height / 2
                    )
                    g = newBounds.createHexagon()
                } else if (type.equalsIgnoreCase("hexagon-inv")) {
                    Bounds newBounds = new Bounds(
                            row % 2 == 0 ? b.minX : b.minX - b.width / 2,
                            b.minY - b.height / 6,
                            row % 2 == 0 ? b.maxX : b.maxX - b.width / 2,
                            b.maxY + b.height / 6,
                    )
                    g = newBounds.createHexagon(true)
                } else if (type.equalsIgnoreCase("triangle")) {
                    g = b.createTriangles()
                }
                c.call(g, column, row)
                y += cellHeight
            }
            x += cellWidth
            y = this.minY
        }
    }

    /**
     * Get a generated grid with the given cell width and height
     * @param cellWidth The cell width
     * @param cellHeight The cell height
     * @param type The cell type (polygon, point, circle/ellipse, hexagon, hexagon-inv, triangle)
     * @return The generated grid as a Geometry
     */
    Geometry getGrid(double cellWidth, double cellHeight, String type = "polygon") {
        List geoms = []
        this.generateGrid(cellWidth, cellHeight, type, {cell, c, r ->
            geoms.add(cell)
        })
        new GeometryCollection(geoms)
    }

    /**
     * Generate a grid with the given cell width and height
     * @param cellWidth The cell width
     * @param cellHeight The cell height
     * @param type The cell type (polygon, point, circle/ellipse, hexagon, hexagon-inv, triangle)
     * @param c A Closure that is called with each Geometry cell with the geometry, the column, and the row
     */
    void generateGrid(double cellWidth, double cellHeight, String type, Closure c) {
        int columns = (height / cellWidth) as int
        if (height % cellWidth != 0) {
            columns++
        }
        int rows = (width / cellHeight) as int
        if (width % cellHeight != 0) {
            rows++
        }
        generateGrid(columns, rows, type, c)
    }

    /**
     * Create a hexagon based on this Bound's extent
     * @param inverted Whether the hexagon is inverted.  If true then
     * the point is north/south, else point is east/west
     * @return A Polygon
     */
    Polygon createHexagon(boolean inverted = false) {
        double w = this.width
        double h = this.height
        if (inverted) {
            new Polygon([[
                    [this.minX + w / 2, this.minY],
                    [this.maxX, this.minY + h * 1/4],
                    [this.maxX, this.minY + h * 3/4],
                    [this.minX + w / 2, this.maxY],
                    [this.minX, this.minY + h * 3/4],
                    [this.minX, this.minY + h * 1/4],
                    [this.minX + w / 2, this.minY]
            ]])
        } else {
            new Polygon([[
                    [this.minX + w * 1/4, this.minY],
                    [this.minX + w * 3/4, this.minY],
                    [this.maxX, this.minY + h / 2],
                    [this.minX + w * 3/4, this.maxY],
                    [this.minX + w * 1/4, this.maxY],
                    [this.minX, this.minY + h/2],
                    [this.minX + w * 1/4, this.minY]
            ]])
        }
    }

    /**
     * Create triangles to fill the Bounds
     * @return A MultiPolygon of 8 triangles
     */
    MultiPolygon createTriangles() {
        double midX = minX + width / 2
        double midY = minY + height / 2
        new MultiPolygon([
            new Polygon(new LinearRing(new Point(minX, midY), new Point(minX, maxY), new Point(midX, maxY), new Point(minX, midY))),
            new Polygon(new LinearRing(new Point(minX, midY), new Point(midX, maxY), new Point(midX, midY), new Point(minX, midY))),
            new Polygon(new LinearRing(new Point(midX, midY), new Point(midX, maxY), new Point(maxX, midY), new Point(midX, midY))),
            new Polygon(new LinearRing(new Point(midX, maxY), new Point(maxX, maxY), new Point(maxX, midY), new Point(midX, maxY))),
            new Polygon(new LinearRing(new Point(minX, minY), new Point(minX, midY), new Point(midX, minY), new Point(minX, minY))),
            new Polygon(new LinearRing(new Point(minX, midY), new Point(midX, minY), new Point(midX, midY), new Point(minX, midY))),
            new Polygon(new LinearRing(new Point(midX, minY), new Point(midX, midY), new Point(maxX, midY), new Point(midX, minY))),
            new Polygon(new LinearRing(new Point(midX, minY), new Point(maxX, minY), new Point(maxX, midY), new Point(midX, minY))),
        ])
    }

    /**
     * Create a rectangle or square based on this Bound's extent with the given number of points and rotation.
     * @param numPoints The number of points
     * @param rotation The rotation angle
     * @return The rectangular Geometry
     */
    Polygon createRectangle(int numPoints = 20, double rotation = 0) {
        Geometry.wrap(createGeometricShapeFactory(numPoints, rotation).createRectangle()) as Polygon
    }

    /**
     * Create an ellipse or circle based on this Bound's extent with the given number of points and rotation.
     * @param numPoints The number of points
     * @param rotation The rotation angle
     * @return The elliptical or circular Geometry
     */
    Polygon createEllipse(int numPoints = 20, double rotation = 0) {
        Geometry.wrap(createGeometricShapeFactory(numPoints, rotation).createEllipse()) as Polygon
    }

    /**
     * Create a squircle based on this Bound's extent with the given number of points and rotation.
     * @param numPoints The number of points
     * @param rotation The rotation angle
     * @return The squircular Geometry
     */
    Polygon createSquircle(int numPoints = 20, double rotation = 0) {
        Geometry.wrap(createGeometricShapeFactory(numPoints, rotation).createSquircle()) as Polygon
    }

    /**
     * Create a super circle based on this Bound's extent with the given number of points and rotation.
     * @param power The positive power
     * @param numPoints The number of points
     * @param rotation The rotation angle
     * @return The super circular Geometry
     */
    Polygon createSuperCircle(double power, int numPoints = 20, double rotation = 0) {
        Geometry.wrap(createGeometricShapeFactory(numPoints, rotation).createSupercircle(power)) as Polygon
    }

    /**
     * Create a LineString arc based on this Bound's extent from the start angle (in radians) for the given angle extent
     * (also in radians) with the given number of points and rotation.
     * @param startAngle The start angle (in radians)
     * @param angleExtent The extent of the angle (in radians)
     * @param numPoints The number of points
     * @param rotation The rotation angle
     * @return The LineString arc
     */
    LineString createArc(double startAngle, double angleExtent, int numPoints = 20, double rotation = 0) {
        Geometry.wrap(createGeometricShapeFactory(numPoints, rotation).createArc(startAngle, angleExtent)) as LineString
    }

    /**
     * Create a Polygon arc based on this Bound's extent from the start angle (in radians) for the given angle extent
     * (also in radians) with the given number of points and rotation.
     * @param startAngle The start angle (in radians)
     * @param angleExtent The extent of the angle (in radians)
     * @param numPoints The number of points
     * @param rotation The rotation angle
     * @return The Polygon arc
     */
    Polygon createArcPolygon(double startAngle, double angleExtent, int numPoints = 20, double rotation = 0) {
        Geometry.wrap(createGeometricShapeFactory(numPoints, rotation).createArcPolygon(startAngle, angleExtent)) as Polygon
    }

    /**
     * Create a sine star based on this Bound's extent with the given number of arms and arm length ratio with the
     * given number of points and rotation.
     * @param numberOfArms The number of arms
     * @param armLengthRatio The arm length ratio
     * @param numPoints The number of points
     * @param rotation The rotation angle
     * @return The sine star Polygon
     */
    Polygon createSineStar(int numberOfArms, double armLengthRatio, int numPoints = 20, double rotation = 0) {
        def shapeFactory = createGeometricShapeFactory(numPoints, rotation, new SineStarFactory())  as SineStarFactory
        shapeFactory.setArmLengthRatio(armLengthRatio)
        shapeFactory.setNumArms(numberOfArms)
        Geometry.wrap(shapeFactory.createSineStar()) as Polygon
    }

    /**
     * Create a GeometricShapeFactory and initialize it with number of points and a rotation.
     * @param numPoints The number of points
     * @param rotation The rotation
     * @param shapeFactory The GeometricShapeFactory
     * @return The initialized GeometricShapeFactory
     */
    private GeometricShapeFactory createGeometricShapeFactory(int numPoints = 100, double rotation = 0.0, GeometricShapeFactory shapeFactory = new GeometricShapeFactory()) {
        shapeFactory.numPoints = numPoints
        shapeFactory.rotation = rotation
        shapeFactory.envelope = env
        shapeFactory
    }

    /**
     * Override the asType method to convert Bounds to Geometry
     * if the Class is Geometry
     * @param type The Class
     * @return The converted Object
     */
    Object asType(Class type) {
        if (type == Geometry) {
            return this.geometry
        } else {
            return super.asType(type)
        }
    }

    /**
     * The string representation
     * @return The string representation
     */
    String toString() {
        "(${minX},${minY},${maxX},${maxY}${if (proj != null){',' + proj.id } else {''}})"
    }

}


<|start_filename|>src/test/groovy/geoscript/render/io/XmlMapReaderTest.groovy<|end_filename|>
package geoscript.render.io

import geoscript.render.Map
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals

class XmlMapReaderTest {

    @Test void getName() {
        MapReader mapReader = new XmlMapReader()
        assertEquals("xml", mapReader.name)
    }

    @Test void readWithOptions() {
        MapReader mapReader = new XmlMapReader()
        String dir = new File("src/test/resources").absolutePath
        Map map = mapReader.read("""<map>
            <width>400</width>
            <height>400</height>
            <type>png</type>
            <proj>EPSG:4326</proj>
            <backgroundColor>blue</backgroundColor>
            <fixAspectRatio>true</fixAspectRatio>
            <layers>
                <layer>
                    <layertype>layer</layertype>
                    <file>${dir}/states.shp</file>
                </layer>
            </layers>
            <bounds>
                <minX>-135.911779</minX>
                <minY>36.993573</minY>
                <maxX>-96.536779</maxX>
                <maxY>51.405899</maxY>
            </bounds>
        </map>""")
        assertEquals(400, map.width)
        assertEquals(400, map.height)
        assertEquals("png", map.type)
        assertEquals("#0000ff", map.backgroundColor)
        assertEquals(true, map.fixAspectRatio)
        assertEquals(1, map.layers.size())
        map.render("target/map1.png")
    }

    @Test void read() {
        MapReader mapReader = new XmlMapReader()
        String dir = new File("src/test/resources").absolutePath
        Map map = mapReader.read("""<map>
            <layers>
                <layer>
                    <layertype>layer</layertype>
                    <file>${dir}/states.shp</file>
                </layer>
            </layers>
        </map>""")
        assertEquals(600, map.width)
        assertEquals(400, map.height)
        assertEquals("png", map.type)
        assertEquals(null, map.backgroundColor)
        assertEquals(true, map.fixAspectRatio)
        assertEquals(1, map.layers.size())
        map.render("target/map2.png")
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/GridItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Color

/**
 * Add a grid to a cartographic document.
 * Useful for positioning items.
 * @author <NAME>
 */
class GridItem extends Item {

    int size = 10

    Color strokeColor = Color.LIGHT_GRAY

    float strokeWidth = 1

    /**
     * Create a GridItem at a location from the top left
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    GridItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the cell size of the grid
     * @param size The cell size
     * @return The GridItem
     */
    GridItem size(int size) {
        this.size = size
        this
    }

    /**
     * Set the stroke color
     * @param color The stroke color
     * @return The GridItem
     */
    GridItem strokeColor(Color color) {
        this.strokeColor = color
        this
    }

    /**
     * Set the stroke width
     * @param strokeWidth The stroke width
     * @return The GridItem
     */
    GridItem strokeWidth(float strokeWidth) {
        this.strokeWidth = strokeWidth
        this
    }

    @Override
    String toString() {
        "GridItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, size = ${size}, stroke-color = ${strokeColor}, stroke-width = ${strokeWidth})"
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/MultiLineStringTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class MultiLineStringTest {

    @Test void constructors() {
        MultiLineString m1 = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        assertEquals "MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", m1.wkt

        MultiLineString m2 = new MultiLineString([[1,2],[3,4]], [[5,6],[7,8]])
        assertEquals "MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", m2.wkt

        MultiLineString m3 = new MultiLineString([new LineString([1,2],[3,4]), new LineString([5,6],[7,8])])
        assertEquals "MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", m3.wkt

        MultiLineString m4 = new MultiLineString([[[1,2],[3,4]], [[5,6],[7,8]]])
        assertEquals "MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", m4.wkt
    }

    @Test void plus() {
        def m1 = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        def m2 = m1 + new LineString([11,12],[13,14])
        assertEquals "MULTILINESTRING ((1 2, 3 4), (5 6, 7 8), (11 12, 13 14))", m2.wkt
        def m3 = new MultiLineString([])
        assertEquals "MULTILINESTRING EMPTY", m3.wkt
        def m4 = m3 + new LineString([11,12],[13,14])
        assertEquals "MULTILINESTRING ((11 12, 13 14))", m4.wkt
    }

    @Test void node() {
        String wkt = "LINESTRING (5.19775390625 51.07421875, 7.52685546875 53.7548828125, 11.65771484375 49.931640625, " +
            "7.52685546875 47.20703125, 9.50439453125 54.501953125, 7.35107421875 52.4365234375, 4.53857421875 " +
            "52.65625, 6.38427734375 50.634765625)"
        def lines = new MultiLineString(Geometry.fromWKT(wkt))
        def noded = lines.node(5)
        assertEquals "MULTILINESTRING ((5.2 51, 5.6 51.6), (5.6 51.6, 6.4 52.6), " +
                "(6.4 52.6, 7.6 53.8, 8.2 53.2), (8.2 53.2, 9 52.4), (9 52.4, 11.6 50, 7.6 47.2, 9 52.4), " +
                "(9 52.4, 9.6 54.6, 8.2 53.2), (8.2 53.2, 7.4 52.4, 6.4 52.6), (6.4 52.6, 4.6 52.6, 5.6 51.6), " +
                "(5.6 51.6, 6.4 50.6))", noded.wkt
    }
    
    @Test void merge() {
        String wkt = "MULTILINESTRING((-29 -27,-30 -29.7,-36 -31,-45 -33),(-45 -33,-46 -32))"
        def lines = Geometry.fromWKT(wkt) as MultiLineString
        def mergedLines = lines.merge()
        assertEquals "MULTILINESTRING ((-29 -27, -30 -29.7, -36 -31, -45 -33, -46 -32))", mergedLines.wkt
    }
    
    @Test void polygonize() {
        def lines = new MultiLineString(
            new LineString ([-5.70068359375, 45.1416015625], [2.47314453125, 53.9306640625]),
            new LineString ([-1.21826171875, 53.9306640625], [8.88916015625, 46.1962890625]),
            new LineString ([0.71533203125, 42.63671875], [7.13134765625, 50.37109375]),
            new LineString ([-5.83251953125, 46.943359375], [4.45068359375, 42.98828125])
        )
        def polygons = lines.node(5).polygonize()
        assertTrue polygons instanceof MultiPolygon
        assertEquals 1, polygons.geometries.size()
    }

    @Test void polygonizeFull() {
        def lines = new MultiLineString(
                new LineString ([-5.70068359375, 45.1416015625], [2.47314453125, 53.9306640625]),
                new LineString ([-1.21826171875, 53.9306640625], [8.88916015625, 46.1962890625]),
                new LineString ([0.71533203125, 42.63671875], [7.13134765625, 50.37109375]),
                new LineString ([-5.83251953125, 46.943359375], [4.45068359375, 42.98828125])
        )
        def results = lines.node(5).polyzonizeFull()
        // Polygons
        assertTrue results.polygons instanceof MultiPolygon
        assertEquals 1, results.polygons.geometries.size()
        // Cut Edges
        assertTrue results.cutEdges instanceof MultiLineString
        assertTrue results.cutEdges.empty
        // Dangles
        assertTrue results.dangles instanceof MultiLineString
        assertEquals 8, results.dangles.geometries.size()
        // Invalid Ring Lines
        assertTrue results.invalidRingLines instanceof MultiLineString
        assertTrue results.invalidRingLines.empty
    }

    @Test void createPointsAlong() {
        def lines = new MultiLineString(
                new LineString ([-5.70068359375, 45.1416015625], [2.47314453125, 53.9306640625]),
                new LineString ([-1.21826171875, 53.9306640625], [8.88916015625, 46.1962890625]),
                new LineString ([0.71533203125, 42.63671875], [7.13134765625, 50.37109375]),
                new LineString ([-5.83251953125, 46.943359375], [4.45068359375, 42.98828125])
        )
        MultiPoint points = lines.createPointsAlong(1)
        assertEquals(49, points.numGeometries)
    }

}


<|start_filename|>src/main/groovy/geoscript/render/io/MapReaders.groovy<|end_filename|>
package geoscript.render.io

/**
 * A Utility for finding all registered MapReaders.
 * @author <NAME>
 */
class MapReaders {

    /**
     * Get a List of all MapReaders
     * @return A List of MapReaders
     */
    static List<Reader> list() {
        ServiceLoader.load(MapReader).iterator().collect()
    }

    /**
     * Find a MapReader by name (json, xml)
     * @param name The name (json, xml)
     * @return A MapReader or null
     */
    static MapReader find(String name) {
        list().find{ MapReader reader ->
            String readerName = reader.class.simpleName
            readerName.toLowerCase().startsWith(name.toLowerCase())
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/io/WktReaderTest.groovy<|end_filename|>
package geoscript.geom.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.*

/**
 * The WktReader UnitTest
 * @author <NAME>
 */
class WktReaderTest {

    @Test void readPoint() {
        WktReader reader = new WktReader()
        String wkt = "POINT (111 -47)"
        Point expected = new Point(111,-47)
        Point actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readLineString() {
        WktReader reader = new WktReader()
        String wkt = "LINESTRING (111 -47, 123 -48, 110 -47)"
        LineString expected = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        LineString actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readLinearRing() {
        WktReader reader = new WktReader()
        String wkt = "LINEARRING (111 -47, 123 -48, 110 -47, 111 -47)"
        LineString expected = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        LineString actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPolygon() {
        WktReader reader = new WktReader()
        String wkt ="POLYGON ((1 1, 10 1, 10 10, 1 10, 1 1), (2 2, 4 2, 4 4, 2 4, 2 2), (5 5, 6 5, 6 6, 5 6, 5 5))"
        Polygon expected = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        Polygon actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPoint() {
        WktReader reader = new WktReader()
        String wkt = "MULTIPOINT ((111 -47), (110 -46.5))"
        MultiPoint expected = new MultiPoint([111,-47],[110,-46.5])
        MultiPoint actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPoint2() {
        WktReader reader = new WktReader()
        String wkt = "MULTIPOINT (111 -47, 110 -46.5)"
        MultiPoint expected = new MultiPoint([111,-47],[110,-46.5])
        MultiPoint actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiLineString() {
        WktReader reader = new WktReader()
        String wkt = "MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))"
        MultiLineString expected = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        MultiLineString actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPolygon() {
        WktReader reader = new WktReader()
        String wkt = "MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8)))"
        MultiPolygon expected = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        MultiPolygon actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readGeometryCollection() {
        WktReader reader = new WktReader()
        String wkt = "GEOMETRYCOLLECTION (POINT (100 0), LINESTRING (101 0, 102 1))"
        GeometryCollection expected = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        GeometryCollection actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readCircularString() {
        WktReader reader = new WktReader()
        String wkt = "CIRCULARSTRING(6.12 10.0, 7.07 7.07, 10.0 0.0)"
        CircularString expected = new CircularString([6.12, 10.0],[7.07, 7.07],[10.0, 0.0])
        CircularString actual = reader.read(wkt)
        assertEquals expected.curvedWkt, actual.curvedWkt
        assertEquals expected.wkt, actual.wkt

        [
                "CIRCULARSTRING (4 1, 7 4, 4 7)",
                "CIRCULARSTRING(1 1, 2 0, 2 0, 1 1, 0 1)"
        ].each {
            assertTrue reader.read(it) instanceof CircularString
        }

    }

    @Test void readCircularRing() {
        WktReader reader = new WktReader()
        String wkt = "CIRCULARSTRING(2.0 1.0, 1.0 2.0, 0.0 1.0, 1.0 0.0, 2.0 1.0)"
        CircularRing expected = new CircularRing([2, 1],[1, 2],[0, 1],[1, 0],[2, 1])
        CircularRing actual = reader.read(wkt)
        assertEquals expected.curvedWkt, actual.curvedWkt
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readCompoundCurve() {
        WktReader reader = new WktReader()
        String wkt = "COMPOUNDCURVE(CIRCULARSTRING(10.0 10.0, 0.0 20.0, -10.0 10.0), (-10.0 10.0, -10.0 0.0, 10.0 0.0, 5.0 5.0))"
        CompoundCurve expected = new CompoundCurve(
                new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]),
                new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [5.0, 5.0])
        )
        CompoundCurve actual = reader.read(wkt)
        assertEquals expected.curvedWkt, actual.curvedWkt
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readCompoundRing() {
        WktReader reader = new WktReader()
        String wkt = "COMPOUNDCURVE(CIRCULARSTRING(10.0 10.0, 0.0 20.0, -10.0 10.0), (-10.0 10.0, -10.0 0.0, 10.0 0.0, 10.0 10.0))"
        CompoundRing expected = new CompoundRing(
                new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]),
                new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [10.0, 10.0])
        )
        CompoundRing actual = reader.read(wkt)
        assertEquals expected.curvedWkt, actual.curvedWkt
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPointWithSrid() {
        WktReader reader = new WktReader()
        String wkt = "SRID=4326;POINT (111 -47)"
        Point expected = new Point(111,-47)
        Point actual = reader.read(wkt)
        assertEquals expected.wkt, actual.wkt
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/SvgCartoFactory.groovy<|end_filename|>
package geoscript.carto

/**
 * A CartoFactory that creates a CartoBuilder that produces SVG documents.
 * @author <NAME>
 */
class SvgCartoFactory implements CartoFactory {

    @Override
    String getName() {
        "svg"
    }

    @Override
    String getMimeType() {
        "application/svg"
    }

    @Override
    CartoBuilder create(PageSize pageSize) {
        new SvgCartoBuilder(pageSize)
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/LineStringTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The LineString unit test
 */
class LineStringTest {
	
    @Test void constructors() {
        def l1 = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        assertEquals "LINESTRING (111 -47, 123 -48, 110 -47)", l1.wkt
		
        def l2 = new LineString([111.0, -47],[123.0, -48],[110.0, -47])
        assertEquals "LINESTRING (111 -47, 123 -48, 110 -47)", l2.wkt

        def l3 = new LineString([ new Point(111.0, -47), new Point(123.0, -48), new Point(110.0, -47)])
        assertEquals "LINESTRING (111 -47, 123 -48, 110 -47)", l3.wkt

        def l4 = new LineString(new Point(111.0, -47), new Point(123.0, -48), new Point(110.0, -47))
        assertEquals "LINESTRING (111 -47, 123 -48, 110 -47)", l4.wkt
    }

    @Test void plus() {
        def line = new LineString([1,2],[3,4],[5,6])
        def multi = line + new LineString([7,8],[9,10],[11,12])
        assertEquals "MULTILINESTRING ((1 2, 3 4, 5 6), (7 8, 9 10, 11 12))", multi.wkt

        def m2 = new MultiLineString([])
        assertEquals "MULTILINESTRING EMPTY", m2.wkt
        def m3 = m2 + new LineString([1,2],[3,4])
        assertEquals "MULTILINESTRING ((1 2, 3 4))", m3.wkt
    }

    @Test void startEndPoint() {
        def line = new LineString([1,2],[3,4],[5,6])
        assertEquals new Point(1,2).wkt, line.startPoint.wkt
        assertEquals new Point(5,6).wkt, line.endPoint.wkt
    }

    @Test void reverse() {
        def line = new LineString([1,2],[3,4],[5,6])
        assertEquals "LINESTRING (1 2, 3 4, 5 6)", line.wkt
        def line2 = line.reverse()
        assertEquals "LINESTRING (5 6, 3 4, 1 2)", line2.wkt
    }

    @Test void isClosed() {
        def line = new LineString([1,2],[3,4],[5,6])
        assertFalse(line.isClosed())
        assertTrue(new LineString([1,1],[1,4],[4,4],[4,1],[1,1]).isClosed())
    }

    @Test void isRing() {
        def line = new LineString([1,2],[3,4],[5,6])
        assertFalse(line.isRing())
        assertTrue(new LineString([1,1],[1,4],[4,4],[4,1],[1,1]).isRing())
    }

    @Test void close() {
        // Not closed
        LineString lineString = new LineString([1,1],[1,4],[4,4],[4,1])
        assertFalse lineString.isClosed()
        LinearRing linearRing = lineString.close()
        assertTrue linearRing.isClosed()
        assertEquals "LINEARRING (1 1, 1 4, 4 4, 4 1, 1 1)", linearRing.wkt
        // Already closed
        lineString = new LineString([1,1],[1,4],[4,4],[4,1],[1,1])
        linearRing = lineString.close()
        assertTrue linearRing.isClosed()
        assertEquals "LINEARRING (1 1, 1 4, 4 4, 4 1, 1 1)", linearRing.wkt

    }

    @Test void closeWithError() {
        assertThrows(IllegalArgumentException) {
            LineString lineString = new LineString([1, 1], [1, 4])
            lineString.close()
        }
    }

    @Test void interpolatePoint() {
        def line = new LineString([
            [1137466.548141059, 650434.9943107369],
            [1175272.4129268457, 648011.541439853],
            [1185935.6055587344, 632986.1336403737]
        ])
        def point = new Point(1153461.34, 649950.30)

        // Interpolate Point Start
        Point pt1 = line.interpolatePoint(0)
        assertEquals line.startPoint.wkt, pt1.wkt

        // Interpolate Point Middle
        Point pt2 = line.interpolatePoint(0.5)
        assertEquals "POINT (1165562.9204493894 648633.9448037925)", pt2.wkt

        // Interpolate Point End
        Point pt3 = line.interpolatePoint(1.0)
        assertEquals line.endPoint.wkt, pt3.wkt
    }

    @Test void locatePoint() {
        def line = new LineString([
            [1137466.548141059, 650434.9943107369],
            [1175272.4129268457, 648011.541439853],
            [1185935.6055587344, 632986.1336403737]
        ])
        def point = new Point(1153461.34, 649950.30)

        double position = line.locatePoint(point)
        assertEquals 0.284, position.round(3), 0.001
    }
    
    @Test void placePoint() {
        def line = new LineString([
            [1137466.548141059, 650434.9943107369],
            [1175272.4129268457, 648011.541439853],
            [1185935.6055587344, 632986.1336403737]
        ])
        def point = new Point(1153461.34, 649950.30)

        Point pt = line.placePoint(point)
        assertEquals "POINT (1153426.8271476042 649411.899502625)", pt.wkt
    }

    @Test void subLine() {
        def line = new LineString([
            [1137466.548141059, 650434.9943107369],
            [1175272.4129268457, 648011.541439853],
            [1185935.6055587344, 632986.1336403737]
        ])
        def point = new Point(1153461.34, 649950.30)

        LineString subLine = line.subLine(0.33, 0.67)
        assertEquals "LINESTRING (1156010.153864557 649246.3016361536, 1175115.6870342216 648021.5879714314)", subLine.wkt
    }

    @Test void createPointsAlong() {
        def line = new LineString([
            [1137466.548141059, 650434.9943107369],
            [1175272.4129268457, 648011.541439853],
            [1185935.6055587344, 632986.1336403737]
        ])
        MultiPoint points = line.createPointsAlong(2000)
        assertEquals(29, points.numGeometries)
    }

    @Test void addPoint() {
        def line = new LineString([new Point(0,0), new Point(5,5)])
        def newLine = line.addPoint(1, new Point(3,3))
        assertEquals 3, newLine.numPoints
        assertEquals "LINESTRING (0 0, 3 3, 5 5)", newLine.wkt
        newLine = newLine + new Point(10,10)
        assertEquals 4, newLine.numPoints
        assertEquals "LINESTRING (0 0, 3 3, 5 5, 10 10)", newLine.wkt
    }

    @Test void removePoint() {
        def line = new LineString([1,1],[2,2],[3,3],[4,4],[5,5])
        line = line.removePoint(1)
        assertEquals "LINESTRING (1 1, 3 3, 4 4, 5 5)", line.wkt
        line = -line
        assertEquals "LINESTRING (1 1, 3 3, 4 4)", line.wkt
        line = line.removePoint(0)
        assertEquals "LINESTRING (3 3, 4 4)", line.wkt
    }

    @Test void setPoint() {
        def line = new LineString([1,1],[2,2],[3,3],[4,4],[5,5])
        line = line.setPoint(0, new Point(0,0))
        assertEquals "LINESTRING (0 0, 2 2, 3 3, 4 4, 5 5)", line.wkt
        line = line.setPoint(4, new Point(10,10))
        assertEquals "LINESTRING (0 0, 2 2, 3 3, 4 4, 10 10)", line.wkt
    }
}


<|start_filename|>src/main/groovy/geoscript/layer/MapAlgebra.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import it.geosolutions.jaiext.jiffle.JiffleBuilder
import java.awt.image.RenderedImage
import org.geotools.coverage.grid.GridCoverage2D
import org.geotools.coverage.grid.GridCoverageFactory

/**
 * The MapAlgebra uses Jiffle to perform Map Algebra for Rasters.
 * @author <NAME>
 */
class MapAlgebra {

    /**
     * Calculate a new Raster by performing the map algebra specified by the Jiffle/MapCalc script.
     * @param script The Jiffle/MapCalc script.  This can be a File, a URL, or a String
     * @param rasters A Map of the input Rasters and their names.
     * @param outputName The output Raster name.
     * @param size The size of the output Raster
     * @param bounds The geographic Bounds of the output Raster
     * @return A new Raster
     */
    Raster calculate(Map options = [:], def script, Map<String, Raster> rasters) {
        // Options
        String outputName = options.get("outputName", "dest")
        List size = options.get("size",null)
        Bounds bounds = options.get("bounds",null)
        // Script can be a File, URL, or String
        if (script instanceof File) {
            script = script.text
        } else if (script instanceof URL) {
            script = script.text
        }
        JiffleBuilder builder = new JiffleBuilder()
        builder.script(script)
        rasters.each {input ->
            builder.source(input.key, (input.value as Raster).coverage.renderedImage)
        }
        if (size == null || size.size() == 0) {
           int width = 0
           int height = 0
           if (rasters != null && rasters.size() > 0) {
               rasters.each{input ->
                  Raster r = input.value
                  List s = r.size
                  width = Math.max(width, s[0])
                  height = Math.max(height, s[1])
               }
               size = [width,height]
           } else {
               size = [500,500]
           }
        }
        if (!bounds) {
            if (rasters != null && rasters.size() > 0) {
                rasters.each{input ->
                    Raster r = input.value
                    if (bounds == null) {
                        bounds = r.bounds
                    }
                    else {
                        bounds.expand(r.bounds)
                    }
                }
            } else {
                bounds = new Bounds(0, 0, size[0], size[1], "EPSG:4326")
            }
        }
        if (!bounds.proj) {
            bounds = new Bounds(bounds.minX, bounds.minX, bounds.maxX, bounds.maxY, "EPSG:4326")
        }
        builder.dest(outputName, size[0] as int, size[1] as int)
        builder.run()
        RenderedImage image = builder.getImage(outputName)
        GridCoverageFactory gridCoverageFactory = new GridCoverageFactory()
        GridCoverage2D grid = gridCoverageFactory.create(outputName, image, bounds.env)
        Raster raster = new Raster(grid)
        return raster
    }
}


<|start_filename|>src/test/groovy/geoscript/feature/io/GeobufReaderTest.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Feature
import geoscript.geom.Point
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

/**
 * The GeobufReader Unit Test
 * @author <NAME>
 */
class GeobufReaderTest {

    @Test void read() {
        GeobufReader reader = new GeobufReader()
        Feature feature = reader.read("0a046e616d650a057072696365100218062a1f0a0c08001a0880e7ed69ffa6e92c6a070a05486f7573656a060a0431322e35")
        assertEquals new Point(111,-47), feature.geom
        assertEquals "House", feature["name"]
        assertEquals "12.5", feature["price"]
    }

}


<|start_filename|>src/test/groovy/geoscript/feature/io/GmlWriterTest.groovy<|end_filename|>
package geoscript.feature.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.Point
import geoscript.feature.Schema
import geoscript.feature.Field
import geoscript.feature.Feature
import geoscript.AssertUtil

/**
 * The GmlWriter UnitTest
 */
class GmlWriterTest {

    @Test void write() {

        // The Schema
        Schema schema = new Schema("houses", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])

        // Create a Feature from a List of values
        Feature feature = new Feature([new Point(111,-47), "House", 12.5], "house1", schema)

        // Write the Feature to a GML String
        GmlWriter writer = new GmlWriter()
        String expected = """<gsf:houses xmlns:gsf="http://geoscript.org/feature" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:gml="http://www.opengis.net/gml" fid="house1">
<gml:name>House</gml:name>
<gsf:geom>
<gml:Point>
<gml:coord>
<gml:X>111.0</gml:X>
<gml:Y>-47.0</gml:Y>
</gml:coord>
</gml:Point>
</gsf:geom>
<gsf:price>12.5</gsf:price>
</gsf:houses>
"""
        String actual = writer.write(feature)
        AssertUtil.assertStringsEqual expected, actual, removeXmlNS: true
    }

}


<|start_filename|>src/test/groovy/geoscript/feature/io/KmlWriterTest.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.AssertUtil
import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.filter.Property
import geoscript.geom.Point
import groovy.xml.StreamingMarkupBuilder
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals

/**
 * The KmlWriter UnitTest
 * @author <NAME>
 */
class KmlWriterTest {

    @Test void write() {
        Schema schema = new Schema("houses", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        Feature feature = new Feature([new Point(111,-47), "House", 12.5], "house1", schema)
        KmlWriter writer = new KmlWriter()
        String expected = """<kml:Placemark xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:kml="http://earth.google.com/kml/2.1" id="house1"><kml:name>House</kml:name><kml:Point><kml:coordinates>111.0,-47.0</kml:coordinates></kml:Point></kml:Placemark>"""
        String actual = writer.write(feature)
        AssertUtil.assertStringsEqual(expected, actual, trim: true)
    }

    @Test void writeUsingMarkupBuilder() {
        Schema schema = new Schema("houses", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        Feature feature = new Feature([new Point(111,-47), "House", 12.5], "house1", schema)
        StreamingMarkupBuilder builder = new StreamingMarkupBuilder()
        KmlWriter writer = new KmlWriter()
        // name (property)
        def actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, feature, namespace: "kml", name: new Property("name"), includeStyle: false, extendedData: false
        } as String
        String expected = "<kml:Placemark xmlns:kml='http://www.opengis.net/kml/2.2'>" +
                "<kml:name>House</kml:name><kml:Point>" +
                "<kml:coordinates>111.0,-47.0</kml:coordinates></kml:Point></kml:Placemark>"
        assertEquals expected, actual
        // extended data, style, name (closure), description (value)
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, feature, namespace: "kml", name: {f -> f.get("name")}, description: "The price is ${feature.get('price')}", color: new geoscript.filter.Color("wheat")
        } as String
        expected = "<kml:Placemark xmlns:kml='http://www.opengis.net/kml/2.2'>" +
                "<kml:name>House</kml:name>" +
                "<kml:description>The price is 12.5</kml:description>" +
                "<kml:Style><kml:IconStyle><kml:color>f5deb3</kml:color></kml:IconStyle></kml:Style>" +
                "<kml:ExtendedData><kml:SchemaData kml:schemaUrl='#houses'>" +
                "<kml:SimpleData kml:name='name'>House</kml:SimpleData>" +
                "<kml:SimpleData kml:name='price'>12.5</kml:SimpleData>" +
                "</kml:SchemaData>" +
                "</kml:ExtendedData><kml:Point><kml:coordinates>111.0,-47.0</kml:coordinates></kml:Point>" +
                "</kml:Placemark>"
        assertEquals expected, actual
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/UTFGridTileRendererTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.FileUtil
import groovy.json.JsonSlurper
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The UTFGridTileRenderer Unit Test
 * @author <NAME>
 */
class UTFGridTileRendererTest {

    @TempDir
    File folder

    @Test
    void render() {
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        File dir = FileUtil.createDir(folder, "states")
        UTFGrid utf = new UTFGrid(dir)
        UTFGridTileRenderer renderer = new UTFGridTileRenderer(utf, shp, [shp.schema.get("STATE_NAME")])
        byte[] data = renderer.render(utf.pyramid.bounds(utf.get(0, 0, 0)))
        assertNotNull data
        assertTrue data.length > 0
        String str = new String(data)
        def json = new JsonSlurper().parseText(str)
        assertNotNull json
        assertNotNull json['grid']
        assertNotNull json['keys']
        assertNotNull json['data']
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/MultiPointTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

class MultiPointTest {
	
    @Test void constructors() {
        def m1 = new MultiPoint([1,2],[3,4])
        assertEquals "MULTIPOINT ((1 2), (3 4))", m1.wkt
		
        def m2 = new MultiPoint(new Point(1,2),new Point(3,4))
        assertEquals "MULTIPOINT ((1 2), (3 4))", m2.wkt

        def pts = [new Point(1,2),new Point(3,4)]
        def m3 = new MultiPoint(pts)
        assertEquals "MULTIPOINT ((1 2), (3 4))", m3.wkt

        def doubles = [[1,2],[3,4]]
        def m4 = new MultiPoint(doubles)
        assertEquals "MULTIPOINT ((1 2), (3 4))", m4.wkt

    }

    @Test void plus() {
        def m1 = new MultiPoint([1,2],[3,4])
        assertEquals "MULTIPOINT ((1 2), (3 4))", m1.wkt
        def m2 = m1 + new Point(5,6)
        assertEquals "MULTIPOINT ((1 2), (3 4))", m1.wkt
        assertEquals "MULTIPOINT ((1 2), (3 4), (5 6))", m2.wkt

        def p1 = new Point(1,2)
        def m3 = p1 + new Point(3,4)
        assertEquals "MULTIPOINT ((1 2), (3 4))", m3.wkt

        def m4 = new MultiPoint([])
        assertEquals "MULTIPOINT EMPTY", m4.wkt
        def m5 = m4 + new Point(1,1)
        assertEquals "MULTIPOINT ((1 1))", m5.wkt
    }
	
}


<|start_filename|>src/test/groovy/geoscript/geom/PolygonTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import org.locationtech.jts.geom.LinearRing as JtsLinearRing

import static org.junit.jupiter.api.Assertions.assertTrue

class PolygonTest {
	
    @Test void constructors() {

        // Create a Polygon from a JTS Polygon
        Polygon p1 = new Polygon(Geometry.factory.createPolygon(new LinearRing([[1,2],[3,4],[5,6],[1,2]]).g,
                [] as JtsLinearRing[]
            ))
        assertEquals "POLYGON ((1 2, 3 4, 5 6, 1 2))", p1.wkt

        // Create a Polygon from a List of List of List of Doubles
        def p2 = new Polygon([[[1,2],[3,4],[5,6],[1,2]]])
        assertEquals "POLYGON ((1 2, 3 4, 5 6, 1 2))", p2.wkt

        // Create a Polygon from a LinearRing (no holes)
        def p3 = new Polygon(new LinearRing([[1,2],[3,4],[5,6],[1,2]]))
        assertEquals "POLYGON ((1 2, 3 4, 5 6, 1 2))", p3.wkt

        // Create a Polygon from a LinearRing and a List of LinearRing holes
        def p4 = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        assertEquals "POLYGON ((1 1, 10 1, 10 10, 1 10, 1 1), (2 2, 4 2, 4 4, 2 4, 2 2), (5 5, 6 5, 6 6, 5 6, 5 5))", p4.wkt

        // Create a Polygon from a List of List of List of Doubles
        def p5 = new Polygon([
                [[1,1], [10,1], [10,10], [1,10], [1,1]],
                [[2,2], [4,2], [4,4], [2,4], [2,2]],
                [[5,5], [6,5], [6,6], [5,6], [5,5]]
            ])
        assertEquals "POLYGON ((1 1, 10 1, 10 10, 1 10, 1 1), (2 2, 4 2, 4 4, 2 4, 2 2), (5 5, 6 5, 6 6, 5 6, 5 5))", p5.wkt

        // Create a Polygon from a repeated List of Doubles with no holes
        def p6 = new Polygon([1,2],[3,4],[5,6],[1,2])
        assertEquals "POLYGON ((1 2, 3 4, 5 6, 1 2))", p6.wkt

        // Create a Polygon from a List of LinearRings
        def p7 = new Polygon(
            [
                new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        assertEquals "POLYGON ((1 1, 10 1, 10 10, 1 10, 1 1), (2 2, 4 2, 4 4, 2 4, 2 2), (5 5, 6 5, 6 6, 5 6, 5 5))", p4.wkt


    }

    @Test void plus() {
        def p = new Polygon([[[1,2],[3,4],[5,6],[1,2]]])
        def m = p + new Polygon([[[11,12],[13,14],[15,16],[11,12]]])
        assertEquals "MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((11 12, 13 14, 15 16, 11 12)))", m.wkt
    }

    @Test void split() {
        Geometry g = new Bounds(0,0,10,10).geometry
        Geometry split1 = g.split(new LineString([[0,0],[10,10]]))
        assertTrue split1 instanceof MultiPolygon
        assertEquals 2, split1.numGeometries
        Geometry split2 = split1.split(new LineString([[0,10], [10,0]]))
        assertTrue split2 instanceof MultiPolygon
        assertEquals 4, split2.numGeometries
    }
}

<|start_filename|>src/test/groovy/geoscript/geom/CircularStringTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The CircularString Unit Test
 * @author <NAME>
 */
class CircularStringTest {

    @Test
    void createWithPoints() {
        CircularString cs = new CircularString(
                new Point(6.12, 10.0),
                new Point(7.07, 7.07),
                new Point(10.0, 0.0)
        )
        assertFalse cs.isClosed()
        assertTrue cs.isCurved()
        assertEquals "CircularString", cs.geometryType
        assertEquals "CIRCULARSTRING (6.12 10.0, 7.07 7.07, 10.0 0.0)", cs.curvedWkt
        assertTrue cs.wkt.startsWith("LINESTRING")
        List pts = cs.controlPoints
        assertEquals 3, pts.size()
        assertEquals new Point(6.12, 10.0), pts[0]
        assertEquals new Point(7.07, 7.07), pts[1]
        assertEquals new Point(10.0, 0.0), pts[2]
        LineString line = cs.linear
        assertTrue line.numPoints > 3
    }

    @Test
    void createWithDoubleLists() {
        CircularString cs = new CircularString(
                [6.12, 10.0],
                [7.07, 7.07],
                [10.0, 0.0]
        )
        assertFalse cs.isClosed()
        assertTrue cs.isCurved()
        assertEquals "CircularString", cs.geometryType
        assertEquals "CIRCULARSTRING (6.12 10.0, 7.07 7.07, 10.0 0.0)", cs.curvedWkt
        assertTrue cs.wkt.startsWith("LINESTRING")
        List pts = cs.controlPoints
        assertEquals 3, pts.size()
        assertEquals new Point(6.12, 10.0), pts[0]
        assertEquals new Point(7.07, 7.07), pts[1]
        assertEquals new Point(10.0, 0.0), pts[2]
        LineString line = cs.linear
        assertTrue line.numPoints > 3
    }

    @Test
    void createWithListOfDoubleLists() {
        CircularString cs = new CircularString([
                [6.12, 10.0],
                [7.07, 7.07],
                [10.0, 0.0]
        ])
        assertFalse cs.isClosed()
        assertTrue cs.isCurved()
        assertEquals "CircularString", cs.geometryType
        assertEquals "CIRCULARSTRING (6.12 10.0, 7.07 7.07, 10.0 0.0)", cs.curvedWkt
        assertTrue cs.wkt.startsWith("LINESTRING")
        List pts = cs.controlPoints
        assertEquals 3, pts.size()
        assertEquals new Point(6.12, 10.0), pts[0]
        assertEquals new Point(7.07, 7.07), pts[1]
        assertEquals new Point(10.0, 0.0), pts[2]
        LineString line = cs.linear
        assertTrue line.numPoints > 3
    }

    @Test
    void createWithListOfPoints() {
        CircularString cs = new CircularString([
                new Point(6.12, 10.0),
                new Point(7.07, 7.07),
                new Point(10.0, 0.0)
        ])
        assertFalse cs.isClosed()
        assertTrue cs.isCurved()
        assertEquals "CircularString", cs.geometryType
        assertEquals "CIRCULARSTRING (6.12 10.0, 7.07 7.07, 10.0 0.0)", cs.curvedWkt
        assertTrue cs.wkt.startsWith("LINESTRING")
        List pts = cs.controlPoints
        assertEquals 3, pts.size()
        assertEquals new Point(6.12, 10.0), pts[0]
        assertEquals new Point(7.07, 7.07), pts[1]
        assertEquals new Point(10.0, 0.0), pts[2]
        cs = Geometry.fromWKT("CIRCULARSTRING (4 1, 7 4, 4 7)")
        LineString line = cs.linear
        assertTrue line.numPoints > 3
    }
}


<|start_filename|>src/test/groovy/geoscript/style/io/UniqueValuesReaderTest.groovy<|end_filename|>
package geoscript.style.io

import geoscript.style.Composite
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

class UniqueValuesReaderTest {

    @TempDir
    File folder

    @Test void readEqualsHex() {
        String str = """AHa=#aa0c74
AHat=#b83b1f
AHcf=#964642
AHh=#78092e
AHpe=#78092e
AHt=#5f025a
AHt3=#e76161
Aa1=#fcedcd
Aa2=#94474b
"""
        UniqueValuesReader reader = new UniqueValuesReader("UnitType", "Polygon")
        Composite symbolizer = reader.read(str)
        assertEquals 9, symbolizer.parts.size()
        // Fill
        assertEquals("#aa0c74", symbolizer.parts[0].parts[0].color.hex)
        assertEquals(1.0, symbolizer.parts[0].parts[0].opacity.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[0].filter.toString())
        // Stroke
        assertEquals("#760851", symbolizer.parts[0].parts[1].color.hex)
        assertEquals(0.5, symbolizer.parts[0].parts[1].width.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[1].filter.toString())
    }

    @Test void readCsvRgb() {
        String str = """Unit,R,G,B
AHa,175,0,111
AHat,192,54,22
AHcf,150,70,72
AHh,109,13,60
AHpe,232,226,82
AHt,99,0,95
AHt3,233,94,94
"""
        UniqueValuesReader reader = new UniqueValuesReader("UnitType", "Polygon")
        Composite symbolizer = reader.read(str)
        assertEquals 7, symbolizer.parts.size()
        // Fill
        assertEquals("#af006f", symbolizer.parts[0].parts[0].color.hex)
        assertEquals(1.0, symbolizer.parts[0].parts[0].opacity.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[0].filter.toString())
        // Stroke
        assertEquals("#7a004d", symbolizer.parts[0].parts[1].color.hex)
        assertEquals(0.5, symbolizer.parts[0].parts[1].width.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[1].filter.toString())
    }

    @Test void readCsvColorNames() {
        String str = """AHa,red
AHat,green
AHcf,blue
AHh,orange
AHpe,wheat
AHt,aqua
AHt3,pink
"""
        UniqueValuesReader reader = new UniqueValuesReader("UnitType", "Polygon")
        Composite symbolizer = reader.read(str)
        assertEquals 7, symbolizer.parts.size()
        // Fill
        assertEquals("#ff0000", symbolizer.parts[0].parts[0].color.hex)
        assertEquals(1.0, symbolizer.parts[0].parts[0].opacity.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[0].filter.toString())
        // Stroke
        assertEquals("#b20000", symbolizer.parts[0].parts[1].color.hex)
        assertEquals(0.5, symbolizer.parts[0].parts[1].width.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[1].filter.toString())
    }

    @Test void readEqualsCsvFromFile() {
        String str = """AHa,#aa0c74
AHat,#b83b1f
AHcf,#964642
AHh,#78092e
AHpe,#78092e
AHt,#5f025a
AHt3,#e76161
Aa1,#fcedcd
Aa2,#94474b
"""
        File file = new File(folder,"colors.txt")
        file.text = str
        UniqueValuesReader reader = new UniqueValuesReader("UnitType", "Polygon")
        Composite symbolizer = reader.read(file)
        assertEquals 9, symbolizer.parts.size()
        // Fill
        assertEquals("#aa0c74", symbolizer.parts[0].parts[0].color.hex)
        assertEquals(1.0, symbolizer.parts[0].parts[0].opacity.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[0].filter.toString())
        // Stroke
        assertEquals("#760851", symbolizer.parts[0].parts[1].color.hex)
        assertEquals(0.5, symbolizer.parts[0].parts[1].width.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[1].filter.toString())
    }

    @Test void readEqualsRgbFromFile() {
        String str = """AHa=175,0,111
AHat=192,54,22
AHcf=150,70,72
AHh=109,13,60
AHpe=232,226,82
AHt=99,0,95
AHt3=233,94,94
"""
        File file = new File(folder,"colors.txt")
        file.text = str
        UniqueValuesReader reader = new UniqueValuesReader("UnitType", "Polygon")
        Composite symbolizer = reader.read(file)
        assertEquals 7, symbolizer.parts.size()
        // Fill
        assertEquals("#af006f", symbolizer.parts[0].parts[0].color.hex)
        assertEquals(1.0, symbolizer.parts[0].parts[0].opacity.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[0].filter.toString())
        // Stroke
        assertEquals("#7a004d", symbolizer.parts[0].parts[1].color.hex)
        assertEquals(0.5, symbolizer.parts[0].parts[1].width.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[1].filter.toString())
    }

    @Test void readEqualsColorNamesFromFile() {
        String str = """AHa=red
AHat=green
AHcf=blue
AHh=orange
AHpe=wheat
AHt=aqua
AHt3=pink
"""
        File file = new File(folder,"colors.txt")
        file.text = str
        UniqueValuesReader reader = new UniqueValuesReader("UnitType", "Polygon")
        Composite symbolizer = reader.read(file)
        assertEquals 7, symbolizer.parts.size()
        // Fill
        assertEquals("#ff0000", symbolizer.parts[0].parts[0].color.hex)
        assertEquals(1.0, symbolizer.parts[0].parts[0].opacity.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[0].filter.toString())
        // Stroke
        assertEquals("#b20000", symbolizer.parts[0].parts[1].color.hex)
        assertEquals(0.5, symbolizer.parts[0].parts[1].width.value as double, 0.1)
        assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[1].filter.toString())
    }

    @Test void readEqualsColorNamesFromInputStream() {
        String str = """AHa=red
AHat=green
AHcf=blue
AHh=orange
AHpe=wheat
AHt=aqua
AHt3=pink
"""
        File file = new File(folder,"colors.txt")
        file.text = str
        file.withInputStream { InputStream inputStream ->
            UniqueValuesReader reader = new UniqueValuesReader("UnitType", "Polygon")
            Composite symbolizer = reader.read(inputStream)
            assertEquals 7, symbolizer.parts.size()
            // Fill
            assertEquals("#ff0000", symbolizer.parts[0].parts[0].color.hex)
            assertEquals(1.0, symbolizer.parts[0].parts[0].opacity.value as double, 0.1)
            assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[0].filter.toString())
            // Stroke
            assertEquals("#b20000", symbolizer.parts[0].parts[1].color.hex)
            assertEquals(0.5, symbolizer.parts[0].parts[1].width.value as double, 0.1)
            assertEquals("[ UnitType = AHa ]", symbolizer.parts[0].parts[1].filter.toString())
        }

    }
}


<|start_filename|>src/test/groovy/geoscript/plot/BoxTest.groovy<|end_filename|>
package geoscript.plot

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import static org.junit.jupiter.api.Assertions.*

/**
 * The Box Unit Test
 * @author <NAME>
 */
class BoxTest {

    @TempDir
    File folder

    @Test void box() {
        Map data = [
            "A":[1,10,20],"B":[45,39,10],"C":[2,4,9],"D":[14,15,19]
        ]
        Chart chart = Box.box(data)
        File file = new File(folder,"box.png")
        chart.save(file)
        assertTrue(file.exists())
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/OSMTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.ServerTestUtil
import okhttp3.HttpUrl
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.*

/**
 * The OSM Unit Test
 * @author <NAME>
 */
class OSMTest {

    @Test
    void getName() {
        OSM osm = new OSM()
        assertEquals "OSM", osm.name
    }

    @Test
    void getWellKnownOSM() {
        OSM osm = OSM.getWellKnownOSM("osm")
        assertEquals "OSM", osm.name
        osm = OSM.getWellKnownOSM("stamen-toner")
        assertEquals "Stamen Toner", osm.name
        osm = OSM.getWellKnownOSM("stamen-toner")
        assertEquals "Stamen Toner", osm.name
        osm = OSM.getWellKnownOSM("stamen-toner-lite")
        assertEquals "Stamen Toner Lite", osm.name
        osm = OSM.getWellKnownOSM("stamen-watercolor")
        assertEquals "Stamen Watercolor", osm.name
        osm = OSM.getWellKnownOSM("stamen-terrain")
        assertEquals "Stamen Terrain", osm.name
        osm = OSM.getWellKnownOSM("mapquest-street")
        assertEquals "MapQuest Street", osm.name
        osm = OSM.getWellKnownOSM("mapquest-satellite")
        assertEquals "MapQuest Satellite", osm.name
        osm = OSM.getWellKnownOSM("wikimedia")
        assertEquals "WikiMedia", osm.name
        assertNull OSM.getWellKnownOSM("N/A")
        assertNull OSM.getWellKnownOSM("")
        assertNull OSM.getWellKnownOSM(null)
    }

    @Test
    void getTileLayerFromMap() {
        OSM osm = TileLayer.getTileLayer([type: 'osm', name: 'osm'])
        assertEquals 'OSM', osm.name
        osm = TileLayer.getTileLayer([type: 'osm'])
        assertEquals 'OSM', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'stamen-toner'])
        assertEquals 'Stamen Toner', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'stamen-toner-lite'])
        assertEquals 'Stamen Toner Lite', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'stamen-watercolor'])
        assertEquals 'Stamen Watercolor', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'stamen-terrain'])
        assertEquals 'Stamen Terrain', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'mapquest-satellite'])
        assertEquals 'MapQuest Satellite', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'mapquest-street'])
        assertEquals 'MapQuest Street', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', url: 'http://a.tile.stamen.com/toner-lite'])
        assertEquals "OSM", osm.name
        osm = TileLayer.getTileLayer([type: 'osm', urls: 'http://a.tile.stamen.com/toner-lite,http://b.tile.stamen.com/toner-lite'])
        assertEquals "OSM", osm.name
    }

    @Test
    void getTileLayerFromString() {
        OSM osm = TileLayer.getTileLayer("type=osm name=osm")
        assertEquals 'OSM', osm.name
        osm = TileLayer.getTileLayer("type=osm")
        assertEquals 'OSM', osm.name
        osm = TileLayer.getTileLayer("osm")
        assertEquals 'OSM', osm.name
        osm = TileLayer.getTileLayer("type=osm name=stamen-toner")
        assertEquals 'Stamen Toner', osm.name
        osm = TileLayer.getTileLayer("stamen-toner")
        assertEquals 'Stamen Toner', osm.name
        osm = TileLayer.getTileLayer("type=osm name=stamen-toner-lite")
        assertEquals 'Stamen Toner Lite', osm.name
        osm = TileLayer.getTileLayer("stamen-toner-lite")
        assertEquals 'Stamen Toner Lite', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'stamen-watercolor'])
        assertEquals 'Stamen Watercolor', osm.name
        osm = TileLayer.getTileLayer([type: 'osm', name: 'stamen-terrain'])
        assertEquals 'Stamen Terrain', osm.name
        osm = TileLayer.getTileLayer("stamen-watercolor")
        assertEquals 'Stamen Watercolor', osm.name
        osm = TileLayer.getTileLayer("type=osm name=mapquest-satellite")
        assertEquals 'MapQuest Satellite', osm.name
        osm = TileLayer.getTileLayer("mapquest-satellite")
        assertEquals 'MapQuest Satellite', osm.name
        osm = TileLayer.getTileLayer("type=osm name=mapquest-street")
        assertEquals 'MapQuest Street', osm.name
        osm = TileLayer.getTileLayer("mapquest-street")
        assertEquals 'MapQuest Street', osm.name
        osm = TileLayer.getTileLayer("type=osm url=http://a.tile.stamen.com/toner-lite")
        assertEquals "OSM", osm.name
        osm = TileLayer.getTileLayer("type=osm urls=http://a.tile.stamen.com/toner-lite,http://b.tile.stamen.com/toner-lite")
        assertEquals "OSM", osm.name
    }

    @Test
    void getTile() {
        ServerTestUtil.withServer { MockWebServer server ->
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.start()
            HttpUrl url = server.url("")

            OSM osm = new OSM("OSM", url.url().toString())
            Tile tile = osm.get(1, 2, 3)
            assertEquals 1, tile.z
            assertEquals 2, tile.x
            assertEquals 3, tile.y
            assertNotNull tile.data

            assertEquals("/1/2/3.png", server.takeRequest().getPath())
        }
    }

    @Test
    void getTiles() {
        ServerTestUtil.withServer { MockWebServer server ->
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.start()
            HttpUrl url = server.url("")

            OSM osm = new OSM("OSM", url.url().toString())
            TileCursor c = osm.tiles(1)
            c.each { Tile t ->
                assertTrue t.z == 1
                assertTrue t.x in [0l, 1l]
                assertTrue t.y in [0l, 1l]
                assertNotNull t.data
            }
            assertEquals("/1/0/0.png", server.takeRequest().getPath())
            assertEquals("/1/1/0.png", server.takeRequest().getPath())
            assertEquals("/1/0/1.png", server.takeRequest().getPath())
            assertEquals("/1/1/1.png", server.takeRequest().getPath())
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/filter/ExpressionTest.groovy<|end_filename|>
package geoscript.filter

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The Expression UnitTest
 * @author <NAME>
 */
class ExpressionTest {

    @Test void constructors() {

        // Create an Expression from a value
        Expression e = new Expression(12)
        assertTrue e.expr instanceof org.opengis.filter.expression.Literal
        assertEquals "12", e.toString()

        // Create an Expression from another Expression
        e = new Expression(e)
        assertTrue e.expr instanceof org.opengis.filter.expression.Literal
        assertEquals "12", e.toString()

        // Create an Expression from a GeoTools Expression
        e = new Expression(Expression.filterFactory.literal(12))
        assertTrue e.expr instanceof org.opengis.filter.expression.Literal
        assertEquals "12", e.toString()
    }

    @Test void evaluate() {
        Expression e = new Expression(12)
        assertEquals 12, e.evaluate()
    }

    @Test void fromCQL() {
        // Literal Number
        Expression e = Expression.fromCQL("12")
        assertTrue e.expr instanceof org.opengis.filter.expression.Literal
        assertEquals "12", e.toString()
        // Literal String
        e = Expression.fromCQL("'A String'")
        assertTrue e.expr instanceof org.opengis.filter.expression.Literal
        assertEquals "A String", e.toString()
        // Property
        e = Expression.fromCQL("NAME")
        assertTrue e instanceof Property
        // Function
        e = Expression.fromCQL("centroid(the_geom)")
        assertTrue e instanceof Function
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/Item.groovy<|end_filename|>
package geoscript.carto

import java.awt.Rectangle

/**
 * The abstract base class for all cartographic items.
 * @author <NAME>
 */
abstract class Item {

    final int x

    final int y

    final int width

    final int height

    /**
     * Create an Item from the top left
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    Item(int x, int y, int width, int height) {
        this.x = x
        this.y = y
        this.width = width
        this.height = height
    }

    /**
     * Get a Rectangle
     * @return A Rectangle
     */
    Rectangle getRectangle() {
        return new Rectangle(x, y, width, height)
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/CursorTest.groovy<|end_filename|>
package geoscript.layer

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.feature.Feature

/**
 * The Cursor UnitTest
 */
class CursorTest {

    @Test void constructors() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        int count = shp.count()
        int counter = 0
        Cursor c = shp.cursor
        while(c.hasNext()) {
            Feature f = c.next()
            counter++
        }
        c.close()
        assertEquals count, counter
    }

    @Test void iterator() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        Cursor c = shp.cursor
        def names = c.collect{f -> f.get("NAME")}
        c.close()
        assertEquals 49, names.size()
    }

    @Test void reset() {

        // Get a Shapefle
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        // Use Cursor to get a List of NAME values
        Cursor c = shp.cursor
        def names = c.collect{f -> f.get("NAME")}
        c.close()
        assertEquals 49, names.size()

        // Reset the Cursor to read again
        c.reset()

        // Use Cursor to get a List of NAME values again
        names = c.collect{f -> f.get("NAME")}
        c.close()
        assertEquals 49, names.size()
    }

    @Test void sortStartAndMax() {
        // Property files don't natively support paging
        File file = new File(getClass().getClassLoader().getResource("points.properties").toURI())
        assertNotNull(file)
        Layer layer = new Property(file)
        assertNotNull(layer)
        // 3 to 4
        Cursor c = layer.getCursor(sort: ["name DESC"], start: 2, max: 2)
        assertEquals "point 2", c.next()['name']
        assertEquals "point 1", c.next()['name']
        assertFalse c.hasNext()
        c.close()
        // 2 to 4
        c = layer.getCursor(sort: ["name DESC"], start: 1, max: 3)
        assertEquals "point 3", c.next()['name']
        assertEquals "point 2", c.next()['name']
        assertEquals "point 1", c.next()['name']
        assertFalse c.hasNext()
        c.close()
        // 1 to 3
        c = layer.getCursor(sort: ["name DESC"], start: 0, max: 3)
        assertEquals "point 4", c.next()['name']
        assertEquals "point 3", c.next()['name']
        assertEquals "point 2", c.next()['name']
        assertFalse c.hasNext()
        c.close()
    }

    @Test void sortStartAndMaxShapfile() {
        File file = new File(getClass().getClassLoader().getResource("points.shp").toURI())
        assertNotNull(file)
        Layer layer = new Shapefile(file)
        assertNotNull(layer)
        // 3 to 4
        Cursor c = layer.getCursor(sort: ["name DESC"], start: 2, max: 2)
        assertEquals "point 2", c.next()['name']
        assertEquals "point 1", c.next()['name']
        assertFalse c.hasNext()
        c.close()
        // 2 to 4
        c = layer.getCursor(sort: ["name DESC"], start: 1, max: 3)
        assertEquals "point 3", c.next()['name']
        assertEquals "point 2", c.next()['name']
        assertEquals "point 1", c.next()['name']
        assertFalse c.hasNext()
        c.close()
        // 1 to 3
        c = layer.getCursor(sort: ["name DESC"], start: 0, max: 3)
        assertEquals "point 4", c.next()['name']
        assertEquals "point 3", c.next()['name']
        assertEquals "point 2", c.next()['name']
        assertFalse c.hasNext()
        c.close()

        // Paging, No Sorting
        c = layer.getCursor(start: 0, max: 3)
        assertEquals "point 1", c.next()['name']
        assertEquals "point 2", c.next()['name']
        assertEquals "point 3", c.next()['name']
        assertFalse c.hasNext()
        c.close()
        c = layer.getCursor(start: 1, max: 2)
        assertEquals "point 2", c.next()['name']
        assertEquals "point 3", c.next()['name']
        assertFalse c.hasNext()
        c.close()
        c = layer.getCursor(start: 2, max: 2)
        assertEquals "point 3", c.next()['name']
        assertEquals "point 4", c.next()['name']
        assertFalse c.hasNext()
        c.close()
        c = layer.getCursor(max: 2)
        assertEquals "point 1", c.next()['name']
        assertEquals "point 2", c.next()['name']
        assertFalse c.hasNext()
        c.close()
    }

}



<|start_filename|>src/test/groovy/geoscript/carto/ScaleTextItemTest.groovy<|end_filename|>
package geoscript.carto

import geoscript.layer.Shapefile
import geoscript.render.Map
import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.*

class ScaleTextItemTest {

    @Test
    void create() {

        Map map = new Map(layers: [
            new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        ])

        ScaleTextItem item = new ScaleTextItem(10,20,300,400)
            .font(new Font("Verdana", Font.BOLD, 14))
            .color(Color.BLUE)
            .verticalAlign(VerticalAlign.MIDDLE)
            .horizontalAlign(HorizontalAlign.CENTER)
            .prefixText("Scale: ")
            .format("#")
            .map(map)

        assertEquals(map, item.map)
        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(item.font.name, "Verdana")
        assertEquals(item.font.style, Font.BOLD)
        assertEquals(item.font.size, 14)
        assertEquals(item.color, Color.BLUE)
        assertEquals(VerticalAlign.MIDDLE, item.verticalAlign)
        assertEquals(HorizontalAlign.CENTER, item.horizontalAlign)
        assertEquals("Scale: ", item.prefixText)
        assertEquals("#", item.format)
        assertTrue(item.toString().startsWith("ScaleTextItem(x = 10, y = 20, width = 300, height = 400, " +
                "prefixText = Scale: , color = java.awt.Color[r=0,g=0,b=255], font = "))
        assertTrue(item.toString().contains("horizontal-align = CENTER, vertical-align = MIDDLE, map = "))
        assertTrue(item.toString().endsWith(", format = #)"))
    }


}


<|start_filename|>src/test/groovy/geoscript/style/ImageOutlineTest.groovy<|end_filename|>
package geoscript.style

import org.geotools.styling.PolygonSymbolizer
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The ImageOutline UnitTest
 * @author <NAME>
 */
class ImageOutlineTest {

    @Test
    void constructors() {
        def outline = new ImageOutline(new Fill("black", 0.75))
        assertNotNull outline.fill
        assertNull outline.stroke

        outline = new ImageOutline(new Stroke("black", 0.35))
        assertNull outline.fill
        assertNotNull outline.stroke
    }

    @Test
    void apply() {
        def outline = new ImageOutline(new Fill("black", 0.75))
        def sym = Symbolizer.styleFactory.createRasterSymbolizer()
        outline.apply(sym)
        assertNotNull sym.imageOutline
        assertTrue sym.imageOutline instanceof PolygonSymbolizer
        assertNotNull sym.imageOutline.fill
        assertNull sym.imageOutline.stroke

        outline = new ImageOutline(new Stroke("black", 0.75))
        sym = Symbolizer.styleFactory.createRasterSymbolizer()
        outline.apply(sym)
        assertNotNull sym.imageOutline
        assertTrue sym.imageOutline instanceof PolygonSymbolizer
        assertNull sym.imageOutline.fill
        assertNotNull sym.imageOutline.stroke
    }

    @Test
    void prepare() {
        def outline = new ImageOutline(new Fill("black", 0.75))
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        outline.prepare(rule)
        def sym = rule.symbolizers[0]
        assertNotNull sym.imageOutline
        assertTrue sym.imageOutline instanceof PolygonSymbolizer
        assertNotNull sym.imageOutline.fill
        assertNull sym.imageOutline.stroke

        outline = new ImageOutline(new Stroke("black", 0.75))
        rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        outline.prepare(rule)
        sym = rule.symbolizers[0]
        assertNotNull sym.imageOutline
        assertTrue sym.imageOutline instanceof PolygonSymbolizer
        assertNull sym.imageOutline.fill
        assertNotNull sym.imageOutline.stroke
    }

    @Test
    void string() {
        def outline = new ImageOutline(new Fill("black", 0.75))
        assertEquals "ImageOutline(fill = Fill(color = #000000, opacity = 0.75))", outline.toString()

        outline = new ImageOutline(new Stroke("black", 0.35))
        assertEquals "ImageOutline(stroke = Stroke(color = #000000, width = 0.35))", outline.toString()
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/ImageTileTest.groovy<|end_filename|>
package geoscript.layer

import org.junit.jupiter.api.Test

import java.awt.image.BufferedImage

import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertNull
import static org.junit.jupiter.api.Assertions.assertNull
import static org.junit.jupiter.api.Assertions.assertNull
import static org.junit.jupiter.api.Assertions.assertTrue

/**
 * The ImageTile Unit Test
 * @author <NAME>
 */
class ImageTileTest {

    @Test
    void tile() {
        ImageTile t = new ImageTile(1, 0, 2)
        assertEquals 1, t.z
        assertEquals 0, t.x
        assertEquals 2, t.y
        assertNull t.data
        assertNull t.image
        assertNull t.base64String
        assertEquals "Tile(x:0, y:2, z:1)", t.toString()

        File f = new File(getClass().getClassLoader().getResource("0.png").toURI())
        t = new ImageTile(1, 0, 2, f.bytes)
        assertEquals 1, t.z
        assertEquals 0, t.x
        assertEquals 2, t.y
        assertNotNull t.data
        assertEquals "Tile(x:0, y:2, z:1)", t.toString()

        BufferedImage image = t.image
        assertNotNull image

        String base64str = t.base64String
        assertNotNull base64str
        assertTrue base64str.length() > 0
    }

}


<|start_filename|>src/main/groovy/geoscript/geom/io/WktReader.groovy<|end_filename|>
package geoscript.geom.io

import geoscript.geom.Geometry
import org.locationtech.jts.io.WKTReader
import org.geotools.geometry.jts.WKTReader2

/**
 * Read a {@link geoscript.geom.Geometry Geometry} from a WKT String.
 * <p><blockquote><pre>
 * WktReader reader = new WktReader()
 * {@link geoscript.geom.Point Point} pt = reader.read("POINT (111 -47)")
 *
 * POINT (111 -47)
 * </pre></blockquote></p>
 * @author <NAME>
 */
class WktReader implements Reader {

    /**
     * The JTS WKTReader
     */
    private final static WKTReader reader = new WKTReader()

    /**
     * The GeoTools WKTReader2 that can support Curved Geometry
     */
    private final static WKTReader2 reader2 = new WKTReader2()

    /**
     * Read a Geometry from a String
     * @param str The String
     * @return A Geometry
     */
    Geometry read(String str) {
        // Handle EWKT (SRID=4326;)
        if (str.startsWith("SRID=")) {
            int start = str.indexOf("SRID=")
            int end = str.indexOf(";", start)
            String srid = str.substring(start, end)
            str = str.substring(end + 1)
        }
        if (str.startsWith("MULTIPOINT")) {
            Geometry.wrap(reader.read(str))
        } else {
            Geometry.wrap(reader2.read(str))
        }
    }
}


<|start_filename|>src/main/groovy/geoscript/layer/RasterTileRenderer.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds

import javax.imageio.ImageIO
import java.awt.image.BufferedImage

/**
 * Just render a Raster by resampling to the desired Bounds and Size.
 * @author <NAME>
 */
class RasterTileRenderer implements TileRenderer {

    private final Raster raster

    RasterTileRenderer(Raster raster) {
        this.raster = raster
    }

    @Override
    byte[] render(Map options = [:], Bounds b) {
        Raster tileRaster = raster.resample(bbox: b, size: [options.get("width",256),options.get("height", 256)])
        BufferedImage image = tileRaster.getBufferedImage()
        def out = new ByteArrayOutputStream()
        ImageIO.write(image, options.get("type", "JPEG"), out)
        out.close()
        out.toByteArray()
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/FormatFactoriesTest.groovy<|end_filename|>
package geoscript.layer

import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertTrue

/**
 * The FormatFactories Unit Test.
 * @author <NAME>
 */
class FormatFactoriesTest {

    @Test void list() {
        List<FormatFactory> factories = FormatFactories.list()
        assertNotNull factories
        assertTrue factories.size() > 0
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/io/XmlCartoReader.groovy<|end_filename|>
package geoscript.carto.io

import geoscript.carto.CartoBuilder
import geoscript.carto.CartoFactories
import geoscript.carto.DateTextItem
import geoscript.carto.GridItem
import geoscript.carto.HorizontalAlign
import geoscript.carto.ImageItem
import geoscript.carto.LegendItem
import geoscript.carto.LineItem
import geoscript.carto.MapItem
import geoscript.carto.NorthArrowItem
import geoscript.carto.NorthArrowStyle
import geoscript.carto.OverviewMapItem
import geoscript.carto.PageSize
import geoscript.carto.ParagraphItem
import geoscript.carto.RectangleItem
import geoscript.carto.ScaleBarItem
import geoscript.carto.ScaleTextItem
import geoscript.carto.TableItem
import geoscript.carto.TextItem
import geoscript.carto.VerticalAlign
import geoscript.filter.Color
import geoscript.render.io.XmlMapReader
import groovy.xml.XmlSlurper
import groovy.xml.slurpersupport.GPathResult

import java.awt.Font
import java.text.DateFormat
import java.text.SimpleDateFormat

/**
 * Read a CartoBuilder from an XML Document
 * @author <NAME>
 */
class XmlCartoReader implements CartoReader {

    @Override
    String getName() {
        "xml"
    }

    @Override
    CartoBuilder read(String str) {
        XmlSlurper xmlSlurper = new XmlSlurper()
        def xml = xmlSlurper.parseText(str)
        XmlMapReader mapReader = new XmlMapReader()
        Map mapItems = [:]
        CartoBuilder cartoBuilder = CartoFactories.findByName(xml.type.text())
            .create(new PageSize(getInt(xml.width.text()), getInt(xml.height.text())))
        xml.items.children().each { GPathResult item ->
            String itemType = item.type.text()
            if (itemType.equalsIgnoreCase("rectangle")) {
                RectangleItem rectangleItem = new RectangleItem(
                    getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text())
                )
                if (!item.strokeColor.isEmpty()) {
                    rectangleItem.strokeColor(getColor(item.strokeColor.text()))
                }
                if (!item.fillColor.isEmpty()) {
                    rectangleItem.fillColor(getColor(item.fillColor.text()))
                }
                if (!item.strokeWidth.isEmpty()) {
                    rectangleItem.strokeWidth(getFloat(item.strokeWidth.text()))
                }
                cartoBuilder.rectangle(rectangleItem)
            } else if (itemType.equalsIgnoreCase("dateText")) {
                DateTextItem dateTextItem = new DateTextItem(
                    getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text())
                )
                if (!item.format.isEmpty()) {
                    dateTextItem.format(item.format.text())
                }
                if (!item.date.isEmpty()) {
                    DateFormat dateFormat = new SimpleDateFormat(dateTextItem.format)
                    dateTextItem.date(dateFormat.parse(item.date.text()))
                }
                if (!item.color.isEmpty()) {
                    dateTextItem.color(getColor(item.color.text()))
                }
                if (!item.font.isEmpty()) {
                    dateTextItem.font(getFont(item.font))
                }
                if (!item.horizontalAlign.isEmpty()) {
                    dateTextItem.horizontalAlign(getHorizontalAlign(item.horizontalAlign.text()))
                }
                if (!item.verticalAlign.isEmpty()) {
                    dateTextItem.verticalAlign(getVerticalAlign(item.verticalAlign.text()))
                }
                cartoBuilder.dateText(dateTextItem)
            } else if (itemType.equalsIgnoreCase("image")) {
                ImageItem imageItem = new ImageItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                imageItem.path(new File(item.path.text()).absoluteFile)
                cartoBuilder.image(imageItem)
            } else if (itemType.equalsIgnoreCase("line")) {
                LineItem lineItem = new LineItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                if (!item.strokeColor.isEmpty()) {
                    lineItem.strokeColor(new Color(item.strokeColor.text()).asColor())
                }
                if (!item.strokeWidth.isEmpty()) {
                    lineItem.strokeWidth(getFloat(item.strokeWidth.text()))
                }
                cartoBuilder.line(lineItem)
            } else if (itemType.equalsIgnoreCase("map")) {
                MapItem mapItem = new MapItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                mapItem.map(mapReader.read(item))
                mapItems[item.name.text()] = mapItem
                cartoBuilder.map(mapItem)
            } else if (itemType.equalsIgnoreCase("overViewMap")) {
                OverviewMapItem overviewMapItem = new OverviewMapItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                overviewMapItem.overviewMap(mapReader.read(item))
                overviewMapItem.linkedMap(mapItems[item.linkedMap.text()].map)
                if (!item.zoomIntoBounds.isEmpty()) {
                    overviewMapItem.zoomIntoBounds(getBoolean(item.zoomIntoBounds.text()))
                }
                if (!item.scaleFactor.isEmpty()) {
                    overviewMapItem.scaleFactor(getDouble(item.scaleFactor.text()))
                }
                cartoBuilder.overViewMap(overviewMapItem)
            } else if (itemType.equalsIgnoreCase("northArrow")) {
                NorthArrowItem northArrowItem = new NorthArrowItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                if (!item.style.isEmpty()) {
                    northArrowItem.style(NorthArrowStyle.valueOf(item.style.text().toUpperCase()))
                }
                if (!item.fillColor1.isEmpty()) {
                    northArrowItem.fillColor1(getColor(item.fillColor1.text()))
                }
                if (!item.fillColor2.isEmpty()) {
                    northArrowItem.fillColor2(getColor(item.fillColor2.text()))
                }
                if (!item.strokeColor1.isEmpty()) {
                    northArrowItem.strokeColor1(getColor(item.strokeColor1.text()))
                }
                if (!item.strokeColor2.isEmpty()) {
                    northArrowItem.strokeColor2(getColor(item.strokeColor2.text()))
                }
                if (!item.strokeWidth.isEmpty()) {
                    northArrowItem.strokeWidth(getFloat(item.strokeWidth.text()))
                }
                if (!item.drawText.isEmpty()) {
                    northArrowItem.drawText(getBoolean(item.drawText.text()))
                }
                if (!item.font.isEmpty()) {
                    northArrowItem.font(getFont(item.font))
                }
                if (!item.textColor.isEmpty()) {
                    northArrowItem.textColor(getColor(item.textColor.text()))
                }
                cartoBuilder.northArrow(northArrowItem)
            } else if (itemType.equalsIgnoreCase("text")) {
                TextItem textItem = new TextItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                textItem.text(item.text.text())
                if (!item.color.isEmpty()) {
                    textItem.color(getColor(item.color.text()))
                }
                if (!item.font.isEmpty()) {
                    textItem.font(getFont(item.font))
                }
                if (!item.horizontalAlign.isEmpty()) {
                    textItem.horizontalAlign(getHorizontalAlign(item.horizontalAlign.text()))
                }
                if (!item.verticalAlign.isEmpty()) {
                    textItem.verticalAlign(getVerticalAlign(item.verticalAlign.text()))
                }
                cartoBuilder.text(textItem)
            } else if (itemType.equalsIgnoreCase("paragraph")) {
                ParagraphItem paragraphItem = new ParagraphItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                paragraphItem.text(item.text.text())
                if (!item.color.isEmpty()) {
                    paragraphItem.color(getColor(item.color.text()))
                }
                if (!item.font.isEmpty()) {
                    paragraphItem.font(getFont(item.font))
                }
                cartoBuilder.paragraph(paragraphItem)
            } else if (itemType.equalsIgnoreCase("scaleText")) {
                ScaleTextItem scaleTextItem = new ScaleTextItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                scaleTextItem.map(mapItems[item.map].map)
                if (!item.format.isEmpty()) {
                    scaleTextItem.format(item.format.text())
                }
                if (!item.prefixText.isEmpty()) {
                    scaleTextItem.prefixText(item.prefixText.text())
                }
                if (!item.color.isEmpty()) {
                    scaleTextItem.color(getColor(item.color.text()))
                }
                if (!item.font.isEmpty()) {
                    scaleTextItem.font(getFont(item.font))
                }
                if (!item.horizontalAlign.isEmpty()) {
                    scaleTextItem.horizontalAlign(getHorizontalAlign(item.horizontalAlign.text()))
                }
                if (!item.verticalAlign.isEmpty()) {
                    scaleTextItem.verticalAlign(getVerticalAlign(item.verticalAlign.text()))
                }
                cartoBuilder.scaleText(scaleTextItem)
            } else if (itemType.equalsIgnoreCase("scaleBar")) {
                ScaleBarItem scaleBarItem = new ScaleBarItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                scaleBarItem.map(mapItems[item.map.text()].map)
                if (!item.strokeColor.isEmpty()) {
                    scaleBarItem.strokeColor(getColor(item.strokeColor.text()))
                }
                if (!item.fillColor.isEmpty()) {
                    scaleBarItem.fillColor(getColor(item.fillColor.text()))
                }
                if (!item.font.isEmpty()) {
                    scaleBarItem.font(getFont(item.font))
                }
                if (!item.strokeWidth.isEmpty()) {
                    scaleBarItem.strokeWidth(getFloat(item.strokeWidth.text()))
                }
                if (!item.border.isEmpty()) {
                    scaleBarItem.border(getInt(item.border.text()))
                }
                if (!item.units.isEmpty()) {
                    scaleBarItem.units(ScaleBarItem.Units.valueOf(item.units.text().toUpperCase()))
                }
                cartoBuilder.scaleBar(scaleBarItem)
            } else if (itemType.equalsIgnoreCase("grid")) {
                GridItem gridItem = new GridItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                if (!item.size.isEmpty()) {
                    gridItem.size(getInt(item.size.text()))
                }
                if (!item.strokeWidth.isEmpty()) {
                    gridItem.strokeWidth(getFloat(item.strokeWidth.text()))
                }
                if (!item.strokeColor.isEmpty()) {
                    gridItem.strokeColor(getColor(item.strokeColor.text()))
                }
                cartoBuilder.grid(gridItem)
            } else if (itemType.equalsIgnoreCase("table")) {
                TableItem tableItem = new TableItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                tableItem.columns(item.columns.children().collect { it.text() })
                item.rows.children().each { GPathResult rowResult ->
                    Map row = [:]
                    rowResult.children().each { row[it.name()] = it.text() }
                    tableItem.row(row)
                }
                cartoBuilder.table(tableItem)
            } else if (itemType.equalsIgnoreCase("legend")) {
                LegendItem legendItem = new LegendItem(getInt(item.x.text()), getInt(item.y.text()), getInt(item.width.text()), getInt(item.height.text()))
                legendItem.addMap(mapItems[item.map.text()].map)
                if (!item.backgroundColor.isEmpty()) {
                    legendItem.backgroundColor(getColor(item.backgroundColor.text()))
                }
                if (!item.title.isEmpty()) {
                    legendItem.title(item.title.text())
                }
                if (!item.titleFont.isEmpty()) {
                    legendItem.titleFont(getFont(item.titleFont))
                }
                if (!item.titleColor.isEmpty()) {
                    legendItem.titleColor(getColor(item.titleColor.text()))
                }
                if (!item.textFont.isEmpty()) {
                    legendItem.textFont(getFont(item.textFont))
                }
                if (!item.textColor.isEmpty()) {
                    legendItem.textColor(getColor(item.textColor.text()))
                }
                if (!item.numberFormat.isEmpty()) {
                    legendItem.numberFormat(item.numberFormat.text())
                }
                if (!item.legendEntryWidth.isEmpty()) {
                    legendItem.legendEntryWidth(getInt(item.legendEntryWidth.text()))
                }
                if (!item.legendEntryHeight.isEmpty()) {
                    legendItem.legendEntryHeight(getInt(item.legendEntryHeight.text()))
                }
                if (!item.gapBetweenEntries.isEmpty()) {
                    legendItem.gapBetweenEntries(getInt(item.gapBetweenEntries.text()))
                }
                cartoBuilder.legend(legendItem)
            }

        }
        cartoBuilder
    }

    private int getInt(String str) {
        Integer.parseInt(str)
    }

    private float getFloat(String str) {
        Float.parseFloat(str)
    }

    private float getDouble(String str) {
        Double.parseDouble(str)
    }

    private boolean getBoolean(String str) {
        Boolean.parseBoolean(str)
    }

    private java.awt.Color getColor(String str) {
        new Color(str).asColor()
    }

    private Font getFont(GPathResult item) {
        String name = item.name.text()
        int style = Font.PLAIN
        if (item.style.text().equalsIgnoreCase("bold")) {
            style = Font.BOLD
        } else if (item.style.text().equalsIgnoreCase("italic")) {
            style = Font.ITALIC
        }
        int size = getInt(item.size.text())
        new Font(name, style, size)
    }

    private HorizontalAlign getHorizontalAlign(String str) {
        HorizontalAlign.valueOf(str.toUpperCase())
    }

    private VerticalAlign getVerticalAlign(String str) {
        VerticalAlign.valueOf(str.toUpperCase())
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/WorldFileTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The WorldFile Unit Test
 * @author <NAME>
 */
class WorldFileTest {

    @TempDir
    File folder

    @Test void create() {

        // Create a WorldFile from bounds and size
        File file = new File(folder, "worldfile.txt")
        WorldFile worldFile = new WorldFile(new Bounds(-123.06, 46.66, -121.15, 47.48), [500,500], file)
        assertEquals(0.003819, worldFile.pixelSize[0], 0.00001)
        assertEquals(-0.00164, worldFile.pixelSize[1], 0.00001)
        assertEquals(0.0, worldFile.rotation[0], 0.01)
        assertEquals(0.0, worldFile.rotation[1], 0.01)
        assertEquals(-123.05809, worldFile.ulc.x, 0.0001)
        assertEquals(47.47918, worldFile.ulc.y, 0.0001)
        assertNotNull(worldFile.file)
        assertEquals(file.name, worldFile.file.name)
        assertEquals("WorldFile: ${file.absolutePath}".toString(), worldFile.toString())
        assertNotNull(worldFile.file.text)

        // Create a WorldFile from a File
        worldFile = new WorldFile(file)
        assertEquals(0.003819, worldFile.pixelSize[0], 0.00001)
        assertEquals(-0.00164, worldFile.pixelSize[1], 0.00001)
        assertEquals(0.0, worldFile.rotation[0], 0.01)
        assertEquals(0.0, worldFile.rotation[1], 0.01)
        assertEquals(-123.05809, worldFile.ulc.x, 0.0001)
        assertEquals(47.47918, worldFile.ulc.y, 0.0001)
        assertNotNull(worldFile.file)
        assertEquals(file.name, worldFile.file.name)
        assertEquals("WorldFile: ${file.absolutePath}".toString(), worldFile.toString())
        assertNotNull(worldFile.file.text)
    }
}

<|start_filename|>src/test/groovy/geoscript/layer/RasterTileRendererTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import geoscript.proj.Projection
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import javax.imageio.ImageIO
import java.awt.image.BufferedImage

import static org.junit.jupiter.api.Assertions.*

class RasterTileRendererTest {

    @TempDir
    private File folder

    @Test
    void render() {
        // Read a Raster which is in EPSG:4326
        File file = new File(getClass().getClassLoader().getResource("raster.tif").toURI())
        assertNotNull(file)
        GeoTIFF geoTIFF = new GeoTIFF(file)
        Raster raster = geoTIFF.read()
        assertNotNull(raster)

        // Resize and Reproject Raster to Web Mercator
        Projection latLonProj = new Projection("EPSG:4326")
        Projection mercatorProj = new Projection("EPSG:3857")
        Bounds latLonBounds = new Bounds(-179.99, -85.0511, 179.99, 85.0511, latLonProj)
        Raster webMercatorRaster = raster.resample(bbox: latLonBounds).reproject(mercatorProj)

        // Generate a Tile
        RasterTileRenderer tileRenderer = new RasterTileRenderer(webMercatorRaster)
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        byte[] bytes = tileRenderer.render(pyramid.bounds(new Tile(0,0,0)), size: [256,256])
        assertTrue(bytes.length > 0)

        // Make sure it can be read as an Image
        BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes))
        File outFile = new File(folder, "raster_0_0_0.jpeg")
        ImageIO.write(image, "JPEG", outFile)
        assertTrue(outFile.exists())
        assertTrue(outFile.length() > 0)
    }

}


<|start_filename|>examples/raster_shadedrelief.groovy<|end_filename|>
import geoscript.layer.GeoTIFF
import geoscript.layer.Raster


File file = new File("raster.tif")
GeoTIFF geoTIFF = new GeoTIFF(file)
Raster raster = geoTIFF.read()

List<Double> degrees = [45.0,90.0,135.0,180.0]
double scale = 1.0

degrees.each { double altitude ->
    degrees.each { double azimuth ->
        Raster shadedReliefRaster = raster.createShadedRelief(scale, altitude, azimuth)
        File outFile = new File("shadedrelief_${altitude}_${azimuth}.tif")
        println "Writing ${outFile}..."
        new GeoTIFF(outFile).write(shadedReliefRaster)
    }
}


<|start_filename|>src/test/groovy/geoscript/carto/TextItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.*

class TextItemTest {

    @Test
    void create() {

        TextItem item = new TextItem(10,20,300,400)
            .font(new Font("Verdana", Font.BOLD, 14))
            .color(Color.BLUE)
            .verticalAlign(VerticalAlign.MIDDLE)
            .horizontalAlign(HorizontalAlign.CENTER)
            .text("Map Title")

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(item.font.name, "Verdana")
        assertEquals(item.font.style, Font.BOLD)
        assertEquals(item.font.size, 14)
        assertEquals(item.color, Color.BLUE)
        assertEquals(VerticalAlign.MIDDLE, item.verticalAlign)
        assertEquals(HorizontalAlign.CENTER, item.horizontalAlign)
        assertEquals("Map Title", item.text)
        assertTrue(item.toString().startsWith("TextItem(x = 10, y = 20, width = 300, height = 400, " +
                "text = Map Title, color = java.awt.Color[r=0,g=0,b=255], font = "))
        assertTrue(item.toString().endsWith("horizontal-align = CENTER, vertical-align = MIDDLE)"))
    }


}


<|start_filename|>src/main/groovy/geoscript/carto/HorizontalAlign.groovy<|end_filename|>
package geoscript.carto

/**
 * Horizontally align text
 * @author <NAME>
 */
enum HorizontalAlign {
    LEFT, CENTER, RIGHT
}

<|start_filename|>src/test/groovy/geoscript/layer/TMSTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.FileUtil
import geoscript.ServerTestUtil
import geoscript.style.Fill
import geoscript.style.Stroke
import okhttp3.HttpUrl
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The TMS Unit Test
 * @author <NAME>
 */
class TMSTest {

    @TempDir
    File folder

    @Test
    void generate() {
        // Generate
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        shp.style = new Fill("wheat") + new Stroke("navy", 0.1)
        File file = FileUtil.createDir(folder, "cache")
        TMS tms = new TMS("States", "png", file, Pyramid.createGlobalMercatorPyramid())
        TileRenderer renderer = new ImageTileRenderer(tms, shp)
        TileGenerator generator = new TileGenerator()
        generator.generate(tms, renderer, 0, 2)
        [
                new File(file, "0/0/0.png"),
                new File(file, "1/0/0.png"),
                new File(file, "1/1/0.png"),
                new File(file, "1/1/1.png"),
                new File(file, "1/0/1.png"),
                new File(file, "2/0/0.png"),
                new File(file, "2/0/1.png"),
                new File(file, "2/0/2.png"),
                new File(file, "2/0/3.png"),
                new File(file, "2/1/0.png"),
                new File(file, "2/1/1.png"),
                new File(file, "2/1/2.png"),
                new File(file, "2/1/3.png"),
                new File(file, "2/2/0.png"),
                new File(file, "2/2/1.png"),
                new File(file, "2/2/2.png"),
                new File(file, "2/2/3.png"),
                new File(file, "2/3/0.png"),
                new File(file, "2/3/1.png"),
                new File(file, "2/3/2.png"),
                new File(file, "2/3/3.png")
        ].each { File f ->
            assertTrue f.exists()
        }
        // Read
        (0..2).each { int z ->
            tms.tiles(z).each { Tile t ->
                assertEquals z, t.z
                assertTrue t.x in [0l, 1l, 2l, 3l]
                assertTrue t.y in [0l, 1l, 2l, 3l]
                assertNotNull t.data
            }
        }
    }

    @Test
    void getTile() {
        ServerTestUtil.withServer { MockWebServer server ->
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.start()
            HttpUrl url = server.url("")

            TMS tms = new TMS("World", "png", url.url(), Pyramid.createGlobalMercatorPyramid())
            Tile tile = tms.get(1, 2, 3)
            assertEquals 1, tile.z
            assertEquals 2, tile.x
            assertEquals 3, tile.y
            assertNotNull tile.data

            assertEquals("/1/2/3.png", server.takeRequest().getPath())
        }
    }



    @Test
    void getTiles() {
        ServerTestUtil.withServer { MockWebServer server ->
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("0.png"))))
            server.start()
            HttpUrl url = server.url("")

            TMS tms = new TMS("World", "png", url.url(), Pyramid.createGlobalMercatorPyramid())
            TileCursor c = tms.tiles(1)
            c.each { Tile t ->
                assertTrue t.z == 1
                assertTrue t.x in [0l, 1l]
                assertTrue t.y in [0l, 1l]
                assertNotNull t.data
            }

            assertEquals("/1/0/0.png", server.takeRequest().getPath())
            assertEquals("/1/1/0.png", server.takeRequest().getPath())
            assertEquals("/1/0/1.png", server.takeRequest().getPath())
            assertEquals("/1/1/1.png", server.takeRequest().getPath())
        }
    }

    @Test
    void delete() {
        File file = new File(getClass().getClassLoader().getResource("0.png").toURI())
        File dir =  FileUtil.createDir(folder, "tiles")
        File tileDir = new File(new File(dir,"0"),"0")
        tileDir.mkdirs()
        File tileFile = new File(tileDir, file.name)
        file.withInputStream { input ->
            tileFile.withOutputStream { out ->
                out << input
            }
        }
        TMS tms = new TMS("Tiles", "png", dir, Pyramid.createGlobalMercatorPyramid())
        Tile t = tms.get(0,0,0)
        assertNotNull t.data
        tms.delete(t)
        t = tms.get(0,0,0)
        assertNull t.data
    }

    @Test
    void deleteTiles() {
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        shp.style = new Fill("wheat") + new Stroke("navy", 0.1)
        File file = FileUtil.createDir(folder, "cache")
        TMS tms = new TMS("States", "png", file, Pyramid.createGlobalMercatorPyramid())
        TileRenderer renderer = new ImageTileRenderer(tms, shp)
        TileGenerator generator = new TileGenerator()
        generator.generate(tms, renderer, 0, 2)
        tms.tiles(1).each { Tile tile ->
            assertNotNull tile
            assertNotNull tile.data
        }
        tms.delete(tms.tiles(1))
        tms.tiles(1).each { Tile tile ->
            assertNotNull tile
            assertNull tile.data
        }
        tms.tiles(2).each { Tile tile ->
            assertNotNull tile
            assertNotNull tile.data
        }
        tms.close()
    }

}


<|start_filename|>src/test/groovy/geoscript/plot/RegressionTest.groovy<|end_filename|>
package geoscript.plot

import geoscript.geom.Bounds
import geoscript.geom.Geometry
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import static org.junit.jupiter.api.Assertions.*

/**
 * The Regression Unit Test
 * @author <NAME>
 */
class RegressionTest {

    @TempDir
    File folder

    @Test void linear() {
        def points = Geometry.createRandomPoints(new Bounds(0,0,100,100).geometry, 10)
        List data = points.geometries.collect{pt ->
            [pt.x,pt.y]
        }
        Chart chart = Regression.linear(data)
        File file = new File(folder,"regression_linear.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void power() {
        def points = Geometry.createRandomPoints(new Bounds(0,0,100,100).geometry, 10)
        List data = points.geometries.collect{pt ->
            [pt.x,pt.y]
        }
        Chart chart = Regression.power(data)
        File file = new File(folder,"regression_power.png")
        chart.save(file)
        assertTrue(file.exists())
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/NorthArrowStyle.groovy<|end_filename|>
package geoscript.carto

enum NorthArrowStyle {
    North,
    NorthEastSouthWest
}

<|start_filename|>src/test/groovy/geoscript/render/MapCubeTest.groovy<|end_filename|>
package geoscript.render

import org.junit.jupiter.api.io.TempDir

import javax.imageio.ImageIO
import geoscript.layer.Renderable
import geoscript.layer.Renderables
import org.geotools.image.test.ImageAssert
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertTrue

class MapCubeTest {

    @TempDir
    File folder

    private File getFile(String resource) {
        return new File(getClass().getClassLoader().getResource(resource).toURI())
    }

    @Test void renderToFile() {
        File file = new File(folder,'mapcube.png')
        List<Renderable> layers = Renderables.getRenderables([
                "layertype=raster source=${getFile('raster.tif')}",
                "layertype=layer file=${getFile('states.shp')} stroke=black stroke-width=1.0 fill=white"
        ])
        MapCube mapCube = new MapCube(title: 'GeoScript MapCube', source: 'GeoScript')
        mapCube.render(layers, file)
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/mapcube.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 150000)
    }

    @Test void renderToOutputStream() {
        File file = new File(folder,'mapcube.png')
        List<Renderable> layers = Renderables.getRenderables([
                "layertype=raster source=${getFile('raster.tif')}",
                "layertype=layer file=${getFile('states.shp')} stroke=black stroke-width=1.0 fill=white"
        ])
        MapCube mapCube = new MapCube(title: 'GeoScript MapCube', source: 'GeoScript')
        file.withOutputStream { OutputStream out ->
            mapCube.render(layers, out)
        }
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/mapcube.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 150000)
    }

    @Test void renderToBytes() {
        File file = new File(folder,'mapcube.png')
        List<Renderable> layers = Renderables.getRenderables([
                "layertype=raster source=${getFile('raster.tif')}",
                "layertype=layer file=${getFile('states.shp')} stroke=black stroke-width=1.0 fill=white"
        ])
        MapCube mapCube = new MapCube(title: 'GeoScript MapCube', source: 'GeoScript')
        byte[] bytes = mapCube.render(layers)
        assertTrue bytes.length > 0
        file.bytes = bytes
        File expectedFile = getFile("geoscript/render/mapcube.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 150000)
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/GeometryCollectionTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

class GeometryCollectionTest {
	
    @Test void constructors() {
        def gc1 = new GeometryCollection(new Point(1,2),new Point(3,4))
        assertEquals "GEOMETRYCOLLECTION (POINT (1 2), POINT (3 4))", gc1.wkt

        def pts = [new Point(1,2),new Point(3,4)]
        def gc2 = new GeometryCollection(pts)
        assertEquals "GEOMETRYCOLLECTION (POINT (1 2), POINT (3 4))", gc2.wkt
    }

    @Test void plus() {
        def gc1 = new GeometryCollection(new Point(1,2),new Point(3,4))
        assertEquals "GEOMETRYCOLLECTION (POINT (1 2), POINT (3 4))", gc1.wkt

        def gc2 = gc1 + new Point(5,6)
        assertEquals "GEOMETRYCOLLECTION (POINT (1 2), POINT (3 4), POINT (5 6))", gc2.wkt

        def gc3 = gc1 + new Point(7,8)
        assertEquals "GEOMETRYCOLLECTION (POINT (1 2), POINT (3 4), POINT (7 8))", gc3.wkt

        def gc4 = new GeometryCollection([])
        assertEquals "GEOMETRYCOLLECTION EMPTY", gc4.wkt
        def gc5 = gc4 + new Point(8,9)
        assertEquals "GEOMETRYCOLLECTION (POINT (8 9))", gc5.wkt
    }

    @Test void slice() {
        GeometryCollection g = Geometry.fromWKT("MULTIPOINT ((1 1), (2 2), (3 3), (4 4), (5 5))")
        assertEquals "MULTIPOINT ((2 2), (3 3))", g.slice(1,3).wkt
        assertEquals "POINT (2 2)", g.slice(1,2).wkt
        assertEquals "MULTIPOINT ((3 3), (4 4), (5 5))", g.slice(2).wkt
        assertEquals "MULTIPOINT ((3 3), (4 4), (5 5))", g.slice(-3).wkt
        assertEquals "MULTIPOINT ((2 2), (3 3))", g.slice(-4, -2).wkt
        assertEquals "MULTIPOINT ((1 1), (2 2), (3 3))", g.slice(0, -2).wkt
    }

    @Test void narrow() {
        GeometryCollection gc = Geometry.fromWKT("GEOMETRYCOLLECTION (POINT (1 2), POINT (3 4), POINT (7 8))")
        assertEquals "MULTIPOINT ((1 2), (3 4), (7 8))", gc.narrow().wkt
        gc = Geometry.fromWKT("GEOMETRYCOLLECTION (POINT (8 9))")
        assertEquals "POINT (8 9)", gc.narrow().wkt
        gc = Geometry.fromWKT("GEOMETRYCOLLECTION (POINT (8 9), LINESTRING (1 1, 5 5))")
        assertEquals "GEOMETRYCOLLECTION (POINT (8 9), LINESTRING (1 1, 5 5))", gc.narrow().wkt
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/LineItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Color

/**
 * Add a line to a cartographic document
 * @author <NAME>
 */
class LineItem extends Item {

    Color strokeColor = Color.BLACK

    float strokeWidth = 1

    /**
     * Create a line from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    LineItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the stroke color
     * @param color The stroke color
     * @return The LineItem
     */
    LineItem strokeColor(Color color) {
        this.strokeColor = color
        this
    }

    /**
     * Set the stroke width
     * @param width The stroke width
     * @return The LineItem
     */
    LineItem strokeWidth(float width) {
        this.strokeWidth = width
        this
    }

    @Override
    String toString() {
        "LineItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, stroke-color = ${strokeColor}, stroke-width = ${strokeWidth})"
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/io/GeoJSONReaderTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.proj.Projection
import geoscript.workspace.Memory
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.layer.Layer

/**
 * The GeoJSONReader UnitTest
 * @author <NAME>
 */
class GeoJSONReaderTest {

    @TempDir
    File folder

    @Test void read() {

        String json = """{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[111,-47]},"properties":{"name":"House","price":12.5},"id":"fid-3eff7fce_131b538ad4c_-8000"},{"type":"Feature","geometry":{"type":"Point","coordinates":[121,-45]},"properties":{"name":"School","price":22.7},"id":"fid-3eff7fce_131b538ad4c_-7fff"}]}"""

        GeoJSONReader reader = new GeoJSONReader()

        // Read from a String
        Layer layer = reader.read(json, uniformSchema: true)
        assertNotNull layer
        assertEquals "geojson", layer.name
        assertTrue layer.workspace instanceof Memory
        assertNull layer.proj
        assertEquals 2, layer.count

        // Read from a String with custom workspace, name, projection
        layer = reader.read(json, workspace: new Memory(), name: "points", projection: new Projection("EPSG:4326"), uniformSchema: true)
        assertNotNull layer
        assertEquals "points", layer.name
        assertTrue layer.workspace instanceof Memory
        assertEquals "EPSG:4326", layer.proj.id
        assertEquals 2, layer.count

        // Read from an InputStream
        ByteArrayInputStream input = new ByteArrayInputStream(json.getBytes("UTF-8"))
        layer = reader.read(input, uniformSchema: true)
        assertNotNull layer
        assertEquals 2, layer.count

        // Read from a File
        File file = new File(folder,"layer.json")
        file.write(json)
        layer = reader.read(file, uniformSchema: true)
        assertNotNull layer
        assertEquals 2, layer.count
    }

    @Test void readWithDifferentProperties() {
        String json = """{"type":"FeatureCollection","features":[
            {"type":"Feature","geometry":{"type":"Point","coordinates":[111,-47]},"properties":{"name":"House"},"id":"fid-3eff7fce_131b538ad4c_-8000"},
            {"type":"Feature","geometry":{"type":"Point","coordinates":[121,-45]},"properties":{"price":22.7},"id":"fid-3eff7fce_131b538ad4c_-7fff"}
        ]}"""

        GeoJSONReader reader = new GeoJSONReader()

        // Read from a String
        Layer layer = reader.read(json)
        assertNotNull layer
        assertEquals "geojson", layer.name
        assertTrue layer.workspace instanceof Memory
        assertNull layer.proj
        assertEquals 2, layer.count
        assertTrue layer.schema.has("name")
        assertTrue layer.schema.has("price")
    }

}


<|start_filename|>src/main/groovy/geoscript/layer/ArcGrid.groovy<|end_filename|>
package geoscript.layer

import geoscript.proj.Projection
import org.apache.commons.io.input.ReaderInputStream
import org.geotools.coverage.grid.io.AbstractGridFormat
import org.geotools.util.factory.GeoTools
import org.geotools.util.factory.Hints
import org.geotools.gce.arcgrid.ArcGridFormat

import java.nio.charset.Charset

/**
 * A Format that can read and write ArcGrids
 * @author <NAME>
 */
class ArcGrid extends Format {

    /**
     * Create a new ArcGrid
     * @param stream The file
     */
    ArcGrid(def stream) {
        super(new org.geotools.gce.arcgrid.ArcGridFormat(), stream)
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param stream The stream (usually a File)
     * @return A Raster
     */
    @Override
    Raster read(Map options = [:]) {
        if (stream instanceof String) {
            String charset = "UTF-8"
            if (options.containsKey("charset")) {
                charset = options.get("charset")
                options.remove(charset)
            }
            stream = new ReaderInputStream(new StringReader(stream), Charset.forName(charset))
        }
        super.read(options, GeoTools.getDefaultHints())
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param proj The Projection
     * @return A Raster
     */
    @Override
    Raster read(Map options = [:], Projection proj) {
        if (stream instanceof String) {
            String charset = "UTF-8"
            if (options.containsKey("charset")) {
                charset = options.get("charset")
                options.remove(charset)
            }
            stream = new ReaderInputStream(new StringReader(stream), Charset.forName(charset))
        }
        super.read(options, proj)
    }

    /**
     * Read a Raster from the stream (usually a File)
     * @param options Optional named parameters that are turned into an array
     * of GeoTools GeneralParameterValues
     * @param hints GeoTools Hints
     * @return A Raster
     */
    @Override
    Raster read(Map options = [:], Hints hints) {
        if (stream instanceof String) {
            String charset = "UTF-8"
            if (options.containsKey("charset")) {
                charset = options.get("charset")
                options.remove(charset)
            }
            stream = new ReaderInputStream(new StringReader(stream), Charset.forName(charset))
        }
        super.read(options, hints)
    }

    /**
     * Write an ArcGrid Raster to a String
     * @param raster The Raster
     * @param format The string format can either be "arc" which is the default, or grass
     * @return A String
     */
    String writeToString(Raster raster, String format = "arc") {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ArcGrid arcGrid = new ArcGrid(out)
        arcGrid.write(raster, GRASS: format.equalsIgnoreCase("grass") ? true : false)
        out.close()
        out.toString()
    }

    /**
     * The ArcGrid FormatFactory
     */
    static class Factory extends FormatFactory<ArcGrid> {

        @Override
        protected List<String> getFileExtensions() {
            ["asc"]
        }

        @Override
        protected Format createFromFormat(AbstractGridFormat gridFormat, Object source) {
            if (gridFormat instanceof ArcGridFormat) {
                new ArcGrid(source)
            }
        }

        @Override
        protected Format createFromFile(File file) {
            new ArcGrid(file)
        }

    }
}


<|start_filename|>src/test/groovy/geoscript/style/HaloTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import geoscript.filter.Color
import geoscript.filter.Expression

/**
 * The Halo Unit Test
 * @author <NAME>
 */
class HaloTest {

    @Test void constructors() {
        Halo halo = new Halo(new Fill("#ffffff"), 1)
        assertEquals "#ffffff", halo.fill.color.value
        assertEquals 1, halo.radius.value, 0.1
        assertEquals "Halo(fill = Fill(color = #ffffff, opacity = 1.0), radius = 1)", halo.toString()

        halo = new Halo(new Fill(new Color("#ffffff")), new Expression(1))
        assertEquals "#ffffff", halo.fill.color.value
        assertEquals 1, halo.radius.value, 0.1
        assertEquals "Halo(fill = Fill(color = #ffffff, opacity = 1.0), radius = 1)", halo.toString()

        halo = new Halo(fill: new Fill("navy"), radius: 2.5)
        assertEquals "#000080", halo.fill.color.value
        assertEquals 2.5, halo.radius.value, 0.1
        assertEquals "Halo(fill = Fill(color = #000080, opacity = 1.0), radius = 2.5)", halo.toString()
    }

    @Test void apply() {
        def text = Symbolizer.styleFactory.createTextSymbolizer()
        Halo halo = new Halo(new Fill("#ffffff"), 1)
        halo.apply(text)
        assertEquals "#ffffff", text.halo.fill.color.value
        assertEquals 1, text.halo.radius.value, 0.1
    }

    @Test void prepare() {
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createTextSymbolizer())
        Halo halo = new Halo(new Fill("#ffffff"), 1)
        halo.prepare(rule)
        def text = rule.symbolizers()[0]
        assertEquals "#ffffff", text.halo.fill.color.value
        assertEquals 1, text.halo.radius.value, 0.1
    }

}


<|start_filename|>src/main/groovy/geoscript/layer/VectorTileRenderer.groovy<|end_filename|>
package geoscript.layer

import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.filter.Filter
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.layer.io.Writer as IOWriter

/**
 * A Vector TileRenderer that can write a single Layer using any gesocript.layer.io.Writer
 * @author <NAME>
 */
class VectorTileRenderer implements TileRenderer {

    /**
     * The Layer
     */
    private Layer layer

    /**
     * The List of Fields to include
     */
    private List fields

    /**
     * The geoscript.layer.io.Writer
     */
    private IOWriter writer

    /**
     * Create a new VectorTileRenderer
     * @param writer The geoscript.layer.io.Writer
     * @param layer The Layer
     * @param fields The List of Fields to include
     */
    VectorTileRenderer(IOWriter writer, Layer layer, List fields) {
        this.writer = writer
        this.layer = layer
        this.fields = fields
    }

    @Override
    byte[] render(Bounds b) {
        // Make sure the Geometry field is included
        if (!fields.contains(layer.schema.geom.name) && !fields.contains(layer.schema.geom)) {
            fields.add(layer.schema.geom)
        }
        // Get the subset of Features for each Layer
        Layer outLayer = new Layer(layer.name, layer.schema.includeFields(fields, layer.name))
        Bounds projectedBounds = b.reproject(layer.proj)
        Geometry boundsGeom = projectedBounds.geometry
        layer.eachFeature(Filter.intersects(layer.schema.geom.name, boundsGeom), { Feature f ->
            // Crop the Geometry to the Bounds
            Geometry geom = f.geom.intersection(boundsGeom)
            // Collect the attributes
            Map attributes = [(outLayer.schema.geom.name): geom]
            fields.each { def fld ->
                attributes.put(fld instanceof Field ? fld.name : fld, f.get(fld))
            }
            // Add the Feature to the output Layer
            outLayer.add(outLayer.schema.feature(attributes))
        })
        // Write the output Layer of cropped Features using the Writer
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(outLayer, out)
        out.close()
        out.toByteArray()
    }
}


<|start_filename|>src/test/groovy/geoscript/feature/io/SchemaReadersTest.groovy<|end_filename|>
package geoscript.feature.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The SchemaReaders Unit Test
 * @author <NAME>
 */
class SchemaReadersTest {

    @Test void list() {
        List<SchemaReader> readers = SchemaReaders.list()
        assertNotNull readers
        assertTrue readers.size() > 0
    }

    @Test void find() {
        SchemaReader reader = SchemaReaders.find("json")
        assertNotNull reader

        reader = SchemaReaders.find("string")
        assertNotNull reader

        reader = SchemaReaders.find("xml")
        assertNotNull reader

        reader = SchemaReaders.find("asdf")
        assertNull reader
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/PointTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

class PointTest {
	
    @Test void constructors() {
        def p1 = new Point(111,-47)
        assertEquals "POINT (111 -47)", p1.wkt

        def p2 = [111,-47] as Point
        assertEquals "POINT (111 -47)", p2.wkt
    }

    @Test void plus() {
        def p1 = new Point(1,2)
        def m1 = p1 + new Point(3,4)
        assertEquals "MULTIPOINT ((1 2), (3 4))", m1.wkt
    }

    @Test void getXY() {
        def p1 = new Point(111,-47)
        assertEquals(111, p1.x, 0.0)
        assertEquals(-47, p1.y, 0.0)
    }

    @Test void getXYZ() {
        def p = new Point(111, -47, 12.34)
        assertEquals(111, p.x, 0.01)
        assertEquals(-47, p.y, 0.01)
        assertEquals(12.34, p.z, 0.01)
    }

    @Test void getAngle() {
        // Default is degrees
        assertEquals(45, new Point(0,0).getAngle(new Point(10,10)), 0.1)
        assertEquals(-45, new Point(0,0).getAngle(new Point(10,-10)), 0.1)
        assertEquals(-135, new Point(0,0).getAngle(new Point(-10,-10)), 0.1)
        assertEquals(135, new Point(0,0).getAngle(new Point(-10,10)), 0.1)
        // Degrees
        assertEquals(45, new Point(0,0).getAngle(new Point(10,10), "degrees"), 0.1)
        assertEquals(-45, new Point(0,0).getAngle(new Point(10,-10), "degrees"), 0.1)
        assertEquals(-135, new Point(0,0).getAngle(new Point(-10,-10), "degrees"), 0.1)
        assertEquals(135, new Point(0,0).getAngle(new Point(-10,10), "degrees"), 0.1)
        // Radians
        assertEquals(0.7853, new Point(0,0).getAngle(new Point(10,10), "radians"), 0.001)
        assertEquals(-0.7853, new Point(0,0).getAngle(new Point(10,-10), "radians"), 0.001)
        assertEquals(-2.3561, new Point(0,0).getAngle(new Point(-10,-10), "radians"), 0.001)
        assertEquals(2.3561, new Point(0,0).getAngle(new Point(-10,10), "radians"), 0.001)
    }

    @Test void getAzimuth() {
        assertEquals(44.75, new Point(0,0).getAzimuth(new Point(10,10)), 0.01)
        assertEquals(135.24, new Point(0,0).getAzimuth(new Point(10,-10)), 0.1)
        assertEquals(-135.24, new Point(0,0).getAzimuth(new Point(-10,-10)), 0.1)
        assertEquals(-44.75, new Point(0,0).getAzimuth(new Point(-10,10)), 0.1)
    }
}

<|start_filename|>src/test/groovy/geoscript/feature/io/KmlReaderTest.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Feature
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The KmlReader Unit Test
 * @author <NAME>
 */
class KmlReaderTest {

    @Test
    void write() {
        String kml = """<kml:Placemark xmlns:kml="http://earth.google.com/kml/2.1" id="house1">
<kml:name>House</kml:name>
<kml:Point>
<kml:coordinates>111.0,-47.0</kml:coordinates>
</kml:Point>
</kml:Placemark>"""
        KmlReader reader = new KmlReader()
        Feature f = reader.read(kml)
        assertNotNull f
        assertEquals("House", f["name"])
        assertEquals("POINT (111 -47)", f.geom.wkt)
        assertNull f["description"]
    }

}


<|start_filename|>src/test/groovy/geoscript/style/DirectoryStyleRepositoryTest.groovy<|end_filename|>
package geoscript.style

import geoscript.FileUtil
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

class DirectoryStyleRepositoryTest {

    @TempDir
    File folder

    @Test
    void sld() {
        File directory = FileUtil.createDir(folder,"sld")

        File file = new File(getClass().getClassLoader().getResource("states.sld").toURI())
        assertNotNull(file)

        StyleRepository styleRepository = new DirectoryStyleRepository(directory)
        styleRepository.save("states", "states", file.text)
        assertTrue(new File(directory, "states.sld").exists())

        String sld = styleRepository.getDefaultForLayer("states")
        assertNotNull(sld)
        assertTrue(sld.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"))

        List<Map<String, String>> styles = styleRepository.getForLayer("states")
        assertEquals(1, styles.size())
        assertEquals("states", styles[0].layerName)
        assertEquals("states", styles[0].styleName)
        assertTrue(styles[0].style.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"))

        styles = styleRepository.getAll()
        assertEquals(1, styles.size())
        assertEquals("states", styles[0].layerName)
        assertEquals("states", styles[0].styleName)
        assertTrue(styles[0].style.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"))

        styleRepository.delete("states","states")
        assertTrue(styleRepository.getAll().isEmpty())
    }

    @Test
    void css() {
        File directory = FileUtil.createDir(folder,"css")

        File file = new File(getClass().getClassLoader().getResource("states.css").toURI())
        assertNotNull(file)

        StyleRepository styleRepository = new DirectoryStyleRepository(directory)
        styleRepository.save("states", "states", file.text, [type: 'css'])
        assertTrue(new File(directory, "states.css").exists())

        String sld = styleRepository.getDefaultForLayer("states")
        assertNotNull(sld)
        assertTrue(sld.startsWith("states {"))

        List<Map<String, String>> styles = styleRepository.getForLayer("states")
        assertEquals(1, styles.size())
        assertEquals("states", styles[0].layerName)
        assertEquals("states", styles[0].styleName)
        assertTrue(styles[0].style.startsWith("states {"))

        styles = styleRepository.getAll()
        assertEquals(1, styles.size())
        assertEquals("states", styles[0].layerName)
        assertEquals("states", styles[0].styleName)
        assertTrue(styles[0].style.startsWith("states {"))

        styleRepository.delete("states","states")
        assertTrue(styleRepository.getAll().isEmpty())
    }

}


<|start_filename|>src/test/groovy/geoscript/style/IconTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.filter.Expression

/**
 * The Icon Unit Test
 * @author <NAME>
 */
class IconTest {

    @Test void constructor() {
        Icon icon = new Icon("http://www.geotools.org/_static/img/geotools-logo.png", "image/png")
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/png", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.png, format = image/png)", icon.toString()
        assertEquals(-1, icon.size.value)

        icon = new Icon(url: "http://www.geotools.org/_static/img/geotools-logo.png", format: "image/png", size: 32)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/png", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.png, format = image/png)", icon.toString()
        assertEquals 32, icon.size.value

        // No format
        icon = new Icon(url: "http://www.geotools.org/_static/img/geotools-logo.png", size: 32)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/png", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.png, format = image/png)", icon.toString()
        assertEquals 32, icon.size.value

        // Short hand format with named parameters
        icon = new Icon(url: "http://www.geotools.org/_static/img/geotools-logo.png", format: "png", size: 32)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/png", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.png, format = image/png)", icon.toString()
        assertEquals 32, icon.size.value

        // Short hand format with full constructor
        icon = new Icon("http://www.geotools.org/_static/img/geotools-logo.png", "png", 32)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/png", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.png, format = image/png)", icon.toString()
        assertEquals 32, icon.size.value

        icon = new Icon(url: "http://www.geotools.org/_static/img/geotools-logo.png", format: "image/png", size: new Expression(32))
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/png", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.png, format = image/png)", icon.toString()
        assertEquals 32, icon.size.value

        icon = new Icon("http://www.geotools.org/_static/img/geotools-logo.png")
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/png", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.png, format = image/png)", icon.toString()
        assertEquals(-1, icon.size.value)

        icon = new Icon("http://www.geotools.org/_static/img/geotools-logo.jpeg")
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.jpeg"), icon.url
        assertTrue icon.url instanceof URL
        assertEquals "image/jpeg", icon.format
        assertEquals "Icon(url = http://www.geotools.org/_static/img/geotools-logo.jpeg, format = image/jpeg)", icon.toString()
        assertEquals(-1, icon.size.value)
    }

    @Test void apply() {
        Icon icon = new Icon("http://www.geotools.org/_static/img/geotools-logo.png", "image/png")
        def pointSym = Symbolizer.styleFactory.createPointSymbolizer()
        icon.apply(pointSym)
        assertNotNull(pointSym.graphic)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), pointSym.graphic.graphicalSymbols()[0].location
        assertEquals "image/png", pointSym.graphic.graphicalSymbols()[0].format
    }

    @Test void prepare() {
        Icon icon = new Icon("http://www.geotools.org/_static/img/geotools-logo.png", "image/png")
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createPointSymbolizer())
        rule.symbolizers().add(Symbolizer.styleFactory.createPolygonSymbolizer())
        icon.prepare(rule)

        def pointSym = rule.symbolizers[0]
        assertNotNull(pointSym.graphic)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), pointSym.graphic.graphicalSymbols()[0].location
        assertEquals "image/png", pointSym.graphic.graphicalSymbols()[0].format
    }

}


<|start_filename|>src/main/groovy/geoscript/geom/io/WkbReader.groovy<|end_filename|>
package geoscript.geom.io

import geoscript.geom.Geometry
import org.locationtech.jts.io.WKBReader

/**
 * Read a {@link geoscript.geom.Geometry Geometry} from a WKB hex String or byte array.
 * <p><blockquote><pre>
 * WkbReader reader = new WkbReader()
 * {@link geoscript.geom.Point Point} pt = reader.read("0000000001405BC00000000000C047800000000000")
 *
 * POINT (111 -47)
 * </pre></blockquote></p>
 * @author <NAME>
 */
class WkbReader implements Reader {

    /**
     * The JTS WKBReader
     */
    private final static WKBReader reader = new WKBReader()

    /**
     * Read a Geometry from a String
     * @param str The String
     * @return A Geometry
     */
    Geometry read(String str) {
        read(WKBReader.hexToBytes(str))
    }

    /**
     * Read a Geometry from a byte array
     * @param bytes The byte array
     * @return A Geometry
     */
    Geometry read(byte[] bytes) {
        Geometry.wrap(reader.read(bytes))
    }

}


<|start_filename|>src/main/groovy/geoscript/workspace/Workspace.groovy<|end_filename|>
package geoscript.workspace

import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.layer.Cursor
import geoscript.layer.Layer
import org.geotools.data.DataStore
import org.geotools.feature.FeatureCollection
import org.geotools.data.collection.ListFeatureCollection
import org.geotools.data.DataStoreFinder
import org.geotools.util.URLs

/**
 * A Workspace is a container of Layers.
 * @author <NAME>
 */
class Workspace {

    /**
     * The GeoTools DataStore
     */
    DataStore ds

    /**
     * Create a new Workspace wrapping a GeoTools DataStore
     * @param The GeoTools DataStore
     */
    Workspace(DataStore ds) {
        if (ds == null) {
            throw new IllegalArgumentException("Can't find Workspace!")
        }
        this.ds = ds
    }

    /**
     * Create a new Workspace with an in Memory Workspace
     */
    Workspace() {
        this(new Memory().ds)
    }

    /**
     * Create a new Workspace from a Map of parameters.
     * @param params The Map of parameters
     */
    Workspace(Map params) {
        this(Workspace.getWorkspace(params).ds)
    }

    /**
     * Create a new Workspace from a parameter string.  The parameter string is space delimited collection of key=value
     * parameters.  If the key or value contains spaces they must be single quoted.
     * @param paramString The parameter string.
     */
    Workspace(String paramString) {
        this(getParametersFromString(paramString))
    }

    /**
     * Get the format
     * @return The Workspace format name
     */
    String getFormat() {
        ds.getClass().getName()
    }

    /**
     * Get a List of Layer names
     * @return A List of Layer names
     */
    List<String> getNames() {
        ds.typeNames.collect{it.toString()}
    }

    /**
     * Get a List of Layers
     * @return A List of Layers
     */
    List<Layer> getLayers() {
        getNames().collect{name -> get(name)}
    }

    /**
     * Whether the Workspace has a Layer by the given name
     * @param name The Layer name
     * @return Whether the Workspace has a Layer by the given name
     */
    boolean has(String name) {
        getNames().contains(name)
    }

    /**
     * Get a Layer by name
     * @param The Layer name
     * @return A Layer
     */
    Layer get(String name) {
        new Layer(this, ds.getFeatureSource(name))
    }


    /**
     * Another way to get a Layer by name.
     * <p><code>Layer layer = workspace["hospitals"]</code><p>
     * @param The Layer name
     * @return A Layer
     */
    Layer getAt(String name) {
        get(name)
    }

    /**
     * Remove a Layer by name from this Workspace
     * @param name The Layer name
     */
    void remove(String name) {
        ds.removeSchema(name)
    }

    /**
     * Remove a Layer by name from this Workspace
     * @param name The Layer name
     */
    void remove(Layer layer) {
        remove(layer.name)
    }

    /**
     * Create a Layer with a List of Fields
     * @param name The new Layer name
     * @param fields A List of Fields (defaults to a "geom", "Geometry" Field)
     * @return A new Layer
     */
    Layer create(String name, List<Field> fields = [new Field("geom","Geometry")]) {
        create(new Schema(name, fields))
    }

    /**
     * Create a Layer with a Schema
     * @param schema The Schema (defaults to a Schema with a single Geometry Field
     * named "geom"
     * @return A new Layer
     */
    Layer create(Schema schema = new Schema([new Field("geom","Geometry")])) {
        ds.createSchema(schema.featureType)
        get(schema.name)
    }

    /**
     * Add a Layer to the Workspace
     * @param layer The Layer to add
     * @return The newly added Layer
     */
    Layer add(Layer layer) {
        add(layer, layer.name)
    }

    /**
     * Add a Layer as a name to the Workspace
     * @param layer The Layer to add
     * @param name The new name of the Layer
     * @param chunk The number of Features to add in one batch
     * @return The newly added Layer
     */
    Layer add(Layer layer, String name, int chunk=1000) {
        List<Field> flds = layer.schema.fields.collect {
            if (it.isGeometry()) {
                return new Field(it.name, it.typ, layer.proj)
            }
            else {
                return new Field(it.name, it.typ)
            }
        }
        Layer l = create(name, flds)
        l.withWriter {geoscript.layer.Writer writer ->
            Cursor c = layer.getCursor()
            while(true) {
                def features = readFeatures(c, l.schema, chunk)
                if (features.isEmpty()) {
                    break
                }
                new Cursor(features).each{Feature f->
                    writer.add(f)
                }
                if (features.size() < chunk) {
                    break
                }
            }
        }
        l
    }

    /**
     * Read Features from a Cursor in Batches.
     * @param cursor The Cursor
     * @param schema The output Schema
     * @param chunk The number of Features to be read
     * @return A GeoTools FeatureCollection
     */
    protected FeatureCollection readFeatures(Cursor cursor, Schema schema, int chunk) {
        int i = 0
        def features = new ListFeatureCollection(schema.featureType)
        while(cursor.hasNext() && i < chunk) {
            Feature f = cursor.next()
            if (f.schema == null) {
                f.schema = schema
            } else if (f.schema != schema) {
                f = schema.feature(f.attributes)
            }
            features.add(f.f)
            i++
        }
        features
    }

    /**
     * Closes the Workspace by disposing of any resources.
     */
    void close() {
        ds.dispose()
    }

    /**
     * Get a Map from a parameter string: "dbtype=h2 database=roads.db"
     * @param str The parameter string is a space delimited collection of key=value parameters.  Use single
     * quotes around key or values with internal spaces.
     * @return A Map of parameters
     */
    private static Map getParametersFromString(String str) {
        Map params = [:]
        for (WorkspaceFactory workspaceFactory : WorkspaceFactories.list()) {
            params = workspaceFactory.getParametersFromString(str)
            if (!params.isEmpty()) {
                break
            }
        }
        if (params.isEmpty()) {
            params.putAll(getParameters(str))
        }
        if (params.isEmpty()) {
            throw new IllegalArgumentException("Unknown Workspace parameter string: ${str}")
        }
        params
    }

    /**
     * Get a List of available GeoTools workspaces (aka DataStores)
     * @return A List of available GeoTools workspace
     */
    static List getWorkspaceNames() {
        DataStoreFinder.availableDataStores.collect{ds ->
            ds.displayName
        }
    }

    /**
     * Get the list of connection parameters for the given workspace
     * @param name The workspace name
     * @return A List of parameters which are represented as a Map with key, type, required keys
     */
    static List getWorkspaceParameters(String name) {
        def ds = DataStoreFinder.availableDataStores.find{ds ->
            if (ds.displayName.equalsIgnoreCase(name)) {
                return ds
            }
        }
        ds.parametersInfo.collect{param ->
            [key: param.name, type: param.type.name, required: param.required]
        }
    }

    /**
     * Get a Workspace from a parameter string
     * @param paramString The parameter string
     * @return A Workspace or null
     */
    static Workspace getWorkspace(String paramString) {
        Workspace w = null
        // Look in WorkspaceFactories first
        for (WorkspaceFactory workspaceFactory : WorkspaceFactories.list()) {
            w = workspaceFactory.create(paramString)
            if (w != null) {
                break
            }
        }
        // Then try unregistered GeoTools DataStores
        if (w == null) {
            w = getWorkspace(getParameters(paramString))
        }
        if (w == null) {
            throw new IllegalArgumentException("Unknown Workspace parameter string: ${paramString}")
        }
        w
    }

    /**
     * Get a connection map from a connection string
     * @param str The connection string
     * @return A connection map
     */
    static Map getParameters(String str) {
        Map params = [:]
        str.split("[ ]+(?=([^\']*\'[^\']*\')*[^\']*\$)").each {
            def parts = it.split("=")
            if (parts.size() > 1) {
                def key = parts[0].trim()
                if ((key.startsWith("'") && key.endsWith("'")) ||
                        (key.startsWith("\"") && key.endsWith("\""))) {
                    key = key.substring(1, key.length() - 1)
                }
                def value = parts[1].trim()
                if ((value.startsWith("'") && value.endsWith("'")) ||
                        (value.startsWith("\"") && value.endsWith("\""))) {
                    value = value.substring(1, value.length() - 1)
                }
                if (key.equalsIgnoreCase("url")) {
                    try {
                        // URLs with a protocol (file: http:)
                        value = new URL(value)
                        File file = URLs.urlToFile(value)
                        if (file != null) {
                            value = URLs.fileToUrl(file.absoluteFile)
                        } else {
                            value = new URL(value)
                        }
                    } catch(MalformedURLException e) {
                        // Files without a protocol
                        value = new File(value).absoluteFile.toURL()
                    }
                }
                params.put(key, value)
            }
        }
        params
    }

    /**
     * Get a Workspace from a connection parameter Map
     * @param params The Map of connection parameters
     * @return A Workspace or null
     */
    static Workspace getWorkspace(Map params) {
        Workspace w = null
        // Look in WorkspaceFactories first
        String type = params.get('type')
        if (type) {
            for (WorkspaceFactory workspaceFactory : WorkspaceFactories.list()) {
                w = workspaceFactory.create(type, params)
                if (w != null) {
                    break
                }
            }
        }
        if (!w) {
            for (WorkspaceFactory workspaceFactory : WorkspaceFactories.list()) {
                w = workspaceFactory.create(params)
                if (w != null) {
                    break
                }
            }
        }
        // Then try unregistered GeoTools DataStores
        if (!w) {
            w = wrap(DataStoreFinder.getDataStore(params))
        }
        w
    }

    /**
     * Use a Workspace within the Closure.  The Workspace will
     * be closed.
     * @param paramString The param string
     * @param closure The Closure that gets the opened Workspace
     */
    static void withWorkspace(String paramString, Closure closure) {
        withWorkspace(Workspace.getWorkspace(paramString), closure)
    }

    /**
     * Use a Workspace within the Closure.  The Workspace will
     * be closed.
     * @param params The parameter Map
     * @param closure The Closure that gets the opened Workspace
     */
    static void withWorkspace(Map params, Closure closure) {
        withWorkspace(Workspace.getWorkspace(params), closure)
    }

    /**
     * Use a Workspace within the Closure.  The Workspace will
     * be closed.
     * @param workspace The Workspace
     * @param closure The Closure that gets the Workspace
     */
    static void withWorkspace(Workspace workspace, Closure closure) {
        try {
            closure.call(workspace)
        } finally {
            workspace.close()
        }
    }

    /**
     * Wrap a GeoTools DataStore in the appropriate GeoScript Workspace
     * @param ds The GeoTools DataStore
     * @return A GeoScript Workspace or null
     */
    static Workspace wrap(DataStore ds) {
        if (ds == null) {
            null
        } else {
            Workspace w
            for (WorkspaceFactory workspaceFactory : WorkspaceFactories.list()) {
                w = workspaceFactory.create(ds)
                if (w != null) {
                    break
                }
            }
            if (!w) {
                w = new Workspace(ds)
            }
            w
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/style/SymbolizerTest.groovy<|end_filename|>
package geoscript.style

import geoscript.AssertUtil
import geoscript.feature.Field
import geoscript.style.io.SLDWriter
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.filter.Filter

import org.geotools.styling.Style
import org.opengis.filter.Filter as GTFilter
import org.geotools.styling.LineSymbolizer
import org.geotools.styling.PointSymbolizer
import org.geotools.styling.PointSymbolizerImpl
import org.geotools.styling.PolygonSymbolizer
import org.geotools.styling.TextSymbolizer

/**
 * The Symbolizer Unit Test
 */
class SymbolizerTest {

    @TempDir
    File folder

    @Test void construct() {

        // Create a Symbolizer
        Symbolizer sym = new Symbolizer()
        assertEquals Filter.PASS, sym.filter
        assertEquals(-1, sym.scale[0], 0.1)
        assertEquals(-1, sym.scale[1], 0.1)
        assertEquals 0, sym.z
        assertTrue sym.options.isEmpty()

        // Add Filter
        assertTrue sym.where(new Filter("name='Washington'")) instanceof Symbolizer
        assertEquals "[ name = Washington ]", sym.filter.toString()

        // Add min and max scale dependencies
        assertTrue sym.range(100, 500) instanceof Symbolizer
        assertEquals 100, sym.scale[0], 0.1
        assertEquals 500, sym.scale[1], 0.1

        // Add Z-index
        assertTrue sym.zindex(5) instanceof Symbolizer
        assertEquals 5, sym.z

        // Make sure asSLD() works
        sym.asSLD()
    }

    @Test void range() {

        // Both named parameters
        Symbolizer sym = new Symbolizer().range(min: 100, max:200)
        assertEquals 100, sym.scale[0], 0.1
        assertEquals 200, sym.scale[1], 0.1

        // Just one named parameters (min)
        sym = new Symbolizer().range(min: 100)
        assertEquals 100, sym.scale[0], 0.1
        assertEquals(-1, sym.scale[1], 0.1)

        // Just one named parameters (max)
        sym = new Symbolizer().range(max: 1000)
        assertEquals(-1, sym.scale[0], 0.1)
        assertEquals 1000, sym.scale[1], 0.1

        // Constructor
        sym = new Symbolizer().range(100, 200)
        assertEquals 100, sym.scale[0], 0.1
        assertEquals 200, sym.scale[1], 0.1

    }
    
    @Test void plus() {
        def composite = new Fill("red") + new Stroke("#ffffff")
        assertTrue composite instanceof Composite
        assertEquals 2, composite.parts.size()
        assertEquals "Composite (Fill(color = #ff0000, opacity = 1.0), Stroke(color = #ffffff, width = 1))", composite.toString()
    }

    @Test void and() {
        def composite = new Fill("red").and(new Stroke("#ffffff"))
        assertTrue composite instanceof Composite
        assertEquals 2, composite.parts.size()
        assertEquals "Composite (Fill(color = #ff0000, opacity = 1.0), Stroke(color = #ffffff, width = 1))", composite.toString()
    }

    @Test void getDefault() {
        def pointSym = Symbolizer.getDefault("point")
        assertTrue pointSym instanceof Shape

        def lineSym = Symbolizer.getDefault("linestring")
        assertTrue lineSym instanceof Stroke

        def polygonSym = Symbolizer.getDefault("polygon")
        assertTrue polygonSym instanceof Composite
        assertEquals 2, polygonSym.parts.size()

        def pointSym2 = Symbolizer.getDefault("point", "black")
        assertTrue pointSym instanceof Shape
        assertEquals pointSym2.color.hex, "#000000"

        def geomSym = Symbolizer.getDefault(null)
        assertTrue geomSym instanceof Composite
        assertEquals 2, geomSym.parts.size()
        assertTrue geomSym.parts[0] instanceof Composite
        assertTrue geomSym.parts[0].parts[0] instanceof Shape
        assertTrue geomSym.parts[0].parts[1] instanceof Fill
        assertTrue geomSym.parts[1] instanceof Stroke

        def polygonSym2 = Symbolizer.getDefault("polygon", color: "wheat", opacity: 0.75)
        assertTrue polygonSym2 instanceof Composite
        assertEquals 2, polygonSym2.parts.size()
        assertEquals 0.75, polygonSym2.parts[0].opacity.value, 0.1
        assertEquals "#f5deb3", polygonSym2.parts[0].color.value
    }

    @Test void buildString() {
        Symbolizer sym = new Symbolizer()
        assertEquals "Fill(color = #0000ff, width = 2)", sym.buildString("Fill", [color: "#0000ff", width: 2])
        sym.where("name = 'Washington'")
        assertEquals "Fill(color = #0000ff, width = 2)[ name = Washington ]", sym.buildString("Fill", [color: "#0000ff", width: 2])
    }

    @Test void createGeoToolsSymbolizer() {
        assertTrue Symbolizer.createGeoToolsSymbolizer(PointSymbolizer.class) instanceof PointSymbolizer
        assertTrue Symbolizer.createGeoToolsSymbolizer(PointSymbolizerImpl.class) instanceof PointSymbolizer
        assertTrue Symbolizer.createGeoToolsSymbolizer(LineSymbolizer.class) instanceof LineSymbolizer
        assertTrue Symbolizer.createGeoToolsSymbolizer(PolygonSymbolizer.class) instanceof PolygonSymbolizer
        assertTrue Symbolizer.createGeoToolsSymbolizer(TextSymbolizer.class) instanceof TextSymbolizer
    }

    @Test void getGeoToolsSymbolizers() {

        def rule = Symbolizer.styleFactory.createRule()
        assertEquals 1, Symbolizer.getGeoToolsSymbolizers(rule, PointSymbolizer).size()
        assertEquals 1, rule.symbolizers().size()
        assertTrue rule.symbolizers()[0] instanceof PointSymbolizer

        assertEquals 1, Symbolizer.getGeoToolsSymbolizers(rule, PointSymbolizer).size()
        assertEquals 1, rule.symbolizers().size()
        assertTrue rule.symbolizers()[0] instanceof PointSymbolizer

        assertEquals 1, Symbolizer.getGeoToolsSymbolizers(rule, TextSymbolizer).size()
        assertEquals 2, rule.symbolizers().size()
        assertTrue rule.symbolizers()[0] instanceof PointSymbolizer
        assertTrue rule.symbolizers()[1] instanceof TextSymbolizer
    }

    @Test void getStyle() {

        // Simple Symbolizer :: Fill
        Symbolizer sym = new Fill("teal")
        Style style = sym.gtStyle
        assertNotNull style
        assertEquals 1, style.featureTypeStyles().size()
        assertEquals 1, style.featureTypeStyles()[0].rules().size()
        assertEquals 0.0, style.featureTypeStyles()[0].rules()[0].minScaleDenominator, 0.1
        assertTrue Double.isInfinite(style.featureTypeStyles()[0].rules()[0].maxScaleDenominator)
        assertEquals style.featureTypeStyles()[0].rules()[0].filter, GTFilter.INCLUDE
        assertEquals 1, style.featureTypeStyles()[0].rules()[0].symbolizers().size()

        PolygonSymbolizer polygonSym = style.featureTypeStyles()[0].rules()[0].symbolizers()[0]
        assertTrue polygonSym instanceof org.geotools.styling.PolygonSymbolizer
        assertEquals "#008080", polygonSym.fill.color.value
        assertEquals 1.0, polygonSym.fill.opacity.value, 0.01

        // Simple Composite: Fill and Hatch
        sym = new Fill("teal").hatch("slash",new Stroke("navy"),6)
        style = sym.gtStyle
        assertNotNull style
        assertEquals 1, style.featureTypeStyles().size()
        assertEquals 1, style.featureTypeStyles()[0].rules().size()
        assertEquals 0.0, style.featureTypeStyles()[0].rules()[0].minScaleDenominator, 0.1
        assertTrue Double.isInfinite(style.featureTypeStyles()[0].rules()[0].maxScaleDenominator)
        assertEquals style.featureTypeStyles()[0].rules()[0].filter, GTFilter.INCLUDE
        assertEquals 1, style.featureTypeStyles()[0].rules()[0].symbolizers().size()

        polygonSym = style.featureTypeStyles()[0].rules()[0].symbolizers()[0]
        assertTrue polygonSym instanceof org.geotools.styling.PolygonSymbolizer
        assertEquals "#008080", polygonSym.fill.color.value
        assertEquals 1.0, polygonSym.fill.opacity.value, 0.01

        assertEquals "shape://slash", polygonSym.fill.graphicFill.graphicalSymbols()[0].wellKnownName.value
        assertEquals "#000080", polygonSym.fill.graphicFill.graphicalSymbols()[0].stroke.color.value
        assertEquals 1.0, polygonSym.fill.graphicFill.graphicalSymbols()[0].stroke.width.value, 0.1
        assertEquals 6, polygonSym.fill.graphicFill.size.value, 0.1

        // Simple Composite :: Fill and Stroke
        sym = new Fill("teal") + new Stroke("navy")
        style = sym.gtStyle
        assertNotNull style
        assertEquals 1, style.featureTypeStyles().size()
        assertEquals 1, style.featureTypeStyles()[0].rules().size()
        assertEquals 0.0, style.featureTypeStyles()[0].rules()[0].minScaleDenominator, 0.1
        assertTrue Double.isInfinite(style.featureTypeStyles()[0].rules()[0].maxScaleDenominator)
        assertEquals style.featureTypeStyles()[0].rules()[0].filter, GTFilter.INCLUDE
        assertEquals 2, style.featureTypeStyles()[0].rules()[0].symbolizers().size()

        polygonSym = style.featureTypeStyles()[0].rules()[0].symbolizers()[0]
        assertTrue polygonSym instanceof org.geotools.styling.PolygonSymbolizer
        assertEquals "#008080", polygonSym.fill.color.value
        assertEquals 1.0, polygonSym.fill.opacity.value, 0.01

        LineSymbolizer lineSym = style.featureTypeStyles()[0].rules()[0].symbolizers()[1]
        assertTrue lineSym instanceof org.geotools.styling.LineSymbolizer
        assertEquals "#000080", lineSym.stroke.color.value
        assertEquals 1.0, lineSym.stroke.width.value, 0.1
    }

    @Test void getStyleWithZindex() {

        Symbolizer sym = (new Fill("red") + new Stroke("blue")) + new Fill("green").zindex(1)
        Style style = sym.gtStyle

        // There should be 2 FeatureTypeStyles
        assertEquals 2, style.featureTypeStyles().size()

        // The 1st FeatureTypesStyle should have 1 rule
        assertEquals 1, style.featureTypeStyles()[0].rules().size()

        // but 2 Symbolizers
        assertEquals 2, style.featureTypeStyles()[0].rules()[0].symbolizers().size()

        // The 1st Symbolizer should be a PolygonSymbolizer
        def polygonSym = style.featureTypeStyles()[0].rules()[0].symbolizers()[0]
        assertTrue polygonSym instanceof PolygonSymbolizer
        assertEquals "#ff0000", polygonSym.fill.color.value
        assertEquals 1.0, polygonSym.fill.opacity.value, 0.01

        // The 2nd Symbolizer should be a LineSymbolizer
        def lineSym = style.featureTypeStyles()[0].rules()[0].symbolizers()[1]
        assertTrue lineSym instanceof LineSymbolizer
        assertEquals "#0000ff", lineSym.stroke.color.value
        assertEquals 1.0, lineSym.stroke.width.value, 0.1

        // The 2nd FeatureTypeStyle should have 1 rule
        assertEquals 1, style.featureTypeStyles()[1].rules().size()

        // and 1 Symbolizer
        assertEquals 1, style.featureTypeStyles()[1].rules()[0].symbolizers().size()

        // which should be a PolygonSymbolizer
        def polygonSym2 = style.featureTypeStyles()[1].rules()[0].symbolizers()[0]
        assertEquals "#008000", polygonSym2.fill.color.value
        assertEquals 1.0, polygonSym2.fill.opacity.value, 0.01
    }

    @Test void getStyleWithScale() {

        Symbolizer sym = (new Fill("red") + new Stroke("blue")).range(-1, 1000) + new Fill("green").range(1000, -1)
        Style style = sym.gtStyle

        // There should only be one FeatureTypeStyle
        assertEquals 1, style.featureTypeStyles().size()

        // but there should be two Rules
        assertEquals 2, style.featureTypeStyles()[0].rules().size()

        // Rule #1
        def rule1 = style.featureTypeStyles()[0].rules()[0]
        assertEquals 0, rule1.minScaleDenominator, 0.1
        assertEquals 1000, rule1.maxScaleDenominator, 0.1
        assertEquals 2, rule1.symbolizers().size()
        assertTrue rule1.symbolizers()[0] instanceof PolygonSymbolizer
        assertTrue rule1.symbolizers()[1] instanceof LineSymbolizer

        // Rule #2
        def rule2 = style.featureTypeStyles()[0].rules()[1]
        assertEquals 1000, rule2.minScaleDenominator, 0.1
        assertTrue Double.isInfinite(rule2.maxScaleDenominator)
        assertEquals 1, rule2.symbolizers().size()
        assertTrue rule2.symbolizers()[0] instanceof PolygonSymbolizer
    }

    @Test void getScaleWithFilter() {

        Symbolizer sym = (new Fill("red") + new Stroke("blue")).where("FOO = 'foo'") + new Fill("green").where("BAR = 'bar'")
        Style style = sym.gtStyle

        // There should only be one FeatureTypeStyle
        assertEquals 1, style.featureTypeStyles().size()

        // but there should be two Rules
        assertEquals 2, style.featureTypeStyles()[0].rules().size()

        // Rule #1
        def rule1 = style.featureTypeStyles()[0].rules()[0]
        assertEquals new Filter("FOO = 'foo'"), new Filter(rule1.filter)
        assertEquals 2, rule1.symbolizers().size()
        assertTrue rule1.symbolizers()[0] instanceof PolygonSymbolizer
        assertTrue rule1.symbolizers()[1] instanceof LineSymbolizer

        // Rule #2
        def rule2 = style.featureTypeStyles()[0].rules()[1]
        assertEquals new Filter("BAR = 'bar'"), new Filter(rule2.filter)
        assertEquals 1, rule2.symbolizers().size()
        assertTrue rule2.symbolizers()[0] instanceof PolygonSymbolizer
    }

    @Test void asSDL() {

        String NEW_LINE = System.getProperty("line.separator")

        String expectedSld = """<?xml version="1.0" encoding="UTF-8"?><sld:StyledLayerDescriptor xmlns="http://www.opengis.net/sld" xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" version="1.0.0">
  <sld:UserLayer>
    <sld:LayerFeatureConstraints>
      <sld:FeatureTypeConstraint/>
    </sld:LayerFeatureConstraints>
    <sld:UserStyle>
      <sld:Name>Default Styler</sld:Name>
      <sld:FeatureTypeStyle>
        <sld:Name>name</sld:Name>
        <sld:Rule>
          <sld:PolygonSymbolizer>
            <sld:Fill>
              <sld:CssParameter name="fill">#f5deb3</sld:CssParameter>
            </sld:Fill>
          </sld:PolygonSymbolizer>
          <sld:LineSymbolizer>
            <sld:Stroke>
              <sld:CssParameter name="stroke">#a52a2a</sld:CssParameter>
            </sld:Stroke>
          </sld:LineSymbolizer>
        </sld:Rule>
      </sld:FeatureTypeStyle>
    </sld:UserStyle>
  </sld:UserLayer>
</sld:StyledLayerDescriptor>"""

        Symbolizer sym = new Fill("wheat") + new Stroke("brown")

        ByteArrayOutputStream out = new ByteArrayOutputStream()
        sym.asSLD(out)
        String sld = out.toString().trim()
        assertNotNull sld
        assertTrue sld.length() > 0
        AssertUtil.assertStringsEqual expectedSld, sld, removeXmlNS: true, trim: true

        File file = new File(folder,"simple.sld")
        sym.asSLD(file)
        sld = file.text.trim()
        assertNotNull sld
        assertTrue sld.length() > 0
        AssertUtil.assertStringsEqual expectedSld, sld, removeXmlNS: true, trim: true

        sld = sym.sld.trim()
        assertNotNull sld
        assertTrue sld.length() > 0
        AssertUtil.assertStringsEqual expectedSld, sld, removeXmlNS: true, trim: true
    }

    @Test
    void compositeAndBlending() {
        String NEW_LINE = System.getProperty("line.separator")
        SLDWriter writer = new SLDWriter(format: false)
        // Symbolizer composite
        Symbolizer sym = new Fill("wheat").composite("source-in")
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("</sld:Fill><sld:VendorOption name=\"composite\">source-in</sld:VendorOption></sld:PolygonSymbolizer>")
        // Symbolizer composite with opacity
        sym = new Fill("wheat").composite("source-in", opacity: 0.45)
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("</sld:Fill><sld:VendorOption name=\"composite\">source-in, 0.45</sld:VendorOption></sld:PolygonSymbolizer>")
        // FeatureType composite and composite-base
        sym = new Fill("wheat").composite("source-in", base: true, symbolizer: false)
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("</sld:Rule><sld:VendorOption name=\"composite\">source-in</sld:VendorOption>" +
                "<sld:VendorOption name=\"composite-base\">true</sld:VendorOption></sld:FeatureTypeStyle>")
    }

    @Test
    void sortBy() {
        String NEW_LINE = System.getProperty("line.separator")
        SLDWriter writer = new SLDWriter(format: false)
        // Single Layer Z-Ordering
        Symbolizer sym = new Fill("wheat").sortBy(["CATEGORY A", "NAME D"])
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("<sld:VendorOption name=\"sortBy\">CATEGORY A,NAME D</sld:VendorOption>")
        sym = new Fill("wheat").sortBy([new Field("CATEGORY","String"), new Field("NAME","String")])
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("<sld:VendorOption name=\"sortBy\">CATEGORY,NAME</sld:VendorOption>")
        sym = new Fill("wheat").sortBy([[field: "CATEGORY", direction: "A"], [field: "NAME", direction: "D"]])
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("<sld:VendorOption name=\"sortBy\">CATEGORY A,NAME D</sld:VendorOption>")
        sym = new Fill("wheat").sortBy([
                [field: new Field("CATEGORY","String"), direction: "A"],
                [field: new Field("NAME","String"), direction: "D"]]
        )
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("<sld:VendorOption name=\"sortBy\">CATEGORY A,NAME D</sld:VendorOption>")
        // Cross Layer Z-Ordering
        sym = new Fill("wheat").sortBy("Group", ["CATEGORY A", "NAME D"])
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("<sld:VendorOption name=\"sortByGroup\">Group</sld:VendorOption><sld:VendorOption name=\"sortBy\">CATEGORY A,NAME D</sld:VendorOption>")
        sym = new Fill("wheat").sortBy("Group", [new Field("CATEGORY","String"), new Field("NAME","String")])
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("<sld:VendorOption name=\"sortByGroup\">Group</sld:VendorOption><sld:VendorOption name=\"sortBy\">CATEGORY,NAME</sld:VendorOption>")
        sym = new Fill("wheat").sortBy("Group", [[field: "CATEGORY", direction: "A"], [field: "NAME", direction: "D"]])
        assertTrue writer.write(sym).trim().replaceAll(NEW_LINE, "")
                .contains("<sld:VendorOption name=\"sortByGroup\">Group</sld:VendorOption><sld:VendorOption name=\"sortBy\">CATEGORY A,NAME D</sld:VendorOption>")
    }

}



<|start_filename|>src/test/groovy/geoscript/layer/TileLayerTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.FileUtil
import geoscript.feature.Feature
import geoscript.geom.Bounds
import geoscript.geom.Point
import geoscript.proj.Projection
import org.geotools.image.test.ImageAssert
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The TileLayer Unit Test
 * @author <NAME>
 */
class TileLayerTest {

    @TempDir
    File folder

    @Test
    void create() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Bounds b = new Bounds(-179.99, -85.0511, 179.99, 85.0511, "EPSG:4326").reproject("EPSG:3857")
        assertEquals "states", layer.name
        assertEquals new Projection("EPSG:3857"), layer.proj
        assertEquals b, layer.bounds
        Pyramid pyramid = layer.pyramid
        assertEquals "EPSG:3857", pyramid.proj.id
        assertEquals b, pyramid.bounds
        assertEquals 256, pyramid.tileWidth
        assertEquals 256, pyramid.tileHeight
        assertEquals Pyramid.Origin.BOTTOM_LEFT, pyramid.origin
        assertEquals 20, pyramid.grids.size()
        pyramid.grids.eachWithIndex { Grid g, int z ->
            assertEquals z, g.z
            int n = Math.pow(2, z)
            assertEquals n, g.width
            assertEquals n, g.height
            assertEquals 156412.0 / n, g.xResolution, 0.01
            assertEquals 156412.0 / n, g.yResolution, 0.01
        }
        layer.close()
    }

    @Test
    void get() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Tile tile = layer.get(4, 2, 3)
        assertNotNull tile
        assertEquals 4, tile.z
        assertEquals 2, tile.x
        assertEquals 3, tile.y
        assertNotNull tile.data
        layer.close()
    }

    @Test
    void put() {
        // Since we are modifying the mbtiles file copy it to a temp file
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        File newFile = new File(folder, "states_temp.mbtiles")
        newFile.withOutputStream { out ->
            file.withInputStream { inp ->
                out << inp
            }
        }
        MBTiles layer = new MBTiles(newFile)

        // Make sure Tile doesn't exist in database
        Tile tile = layer.get(10, 0, 0)
        assertNotNull tile
        assertEquals 10, tile.z
        assertEquals 0, tile.x
        assertEquals 0, tile.y
        assertNull tile.data

        // Load a tile image
        File f = new File(getClass().getClassLoader().getResource("0.png").toURI())
        tile.data = f.bytes

        // Save Tile and make sure it saved correctly by getting it again
        layer.put(tile)
        tile = layer.get(10, 0, 0)
        assertNotNull tile
        assertEquals 10, tile.z
        assertEquals 0, tile.x
        assertEquals 0, tile.y
        assertNotNull tile.data
        layer.close()
    }

    @Test
    void tilesByZoomLevel() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        TileCursor cursor = layer.tiles(1)
        assertEquals 1, cursor.z
        assertEquals 0, cursor.minX
        assertEquals 0, cursor.minY
        assertEquals 1, cursor.maxX
        assertEquals 1, cursor.maxY
        assertEquals 2, cursor.width
        assertEquals 2, cursor.height
        assertEquals 4, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 1, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 4, c
        layer.close()
    }

    @Test
    void tilesByTileCoordinates() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        TileCursor cursor = layer.tiles(2, 1, 2, 3, 3)
        assertEquals 2, cursor.z
        assertEquals 1, cursor.minX
        assertEquals 2, cursor.minY
        assertEquals 3, cursor.maxX
        assertEquals 3, cursor.maxY
        assertEquals 3, cursor.width
        assertEquals 2, cursor.height
        assertEquals 6, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 2, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 6, c
        layer.close()
    }

    @Test
    void tilesByBoundsAndZoomLevel() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Bounds b = new Bounds(-123.09, 46.66, -121.13, 47.48, "EPSG:4326").reproject("EPSG:3857")
        TileCursor cursor = layer.tiles(b, 3)
        assertEquals 3, cursor.z
        assertEquals 1, cursor.minX
        assertEquals 5, cursor.minY
        assertEquals 1, cursor.maxX
        assertEquals 5, cursor.maxY
        assertEquals 1, cursor.width
        assertEquals 1, cursor.height
        assertEquals 1, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 3, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 1, c
        layer.close()
    }

    @Test
    void tilesByBoundsAndResolutions() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        TileCursor cursor = layer.tiles(b, b.width / 400, b.height / 300)
        assertEquals 4, cursor.z
        assertEquals 2, cursor.minX
        assertEquals 9, cursor.minY
        assertEquals 5, cursor.maxX
        assertEquals 10, cursor.maxY
        assertEquals 4, cursor.width
        assertEquals 2, cursor.height
        assertEquals 8, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 4, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 8, c
        layer.close()
    }

    @Test
    void tilesByBoundsAndImageSize() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        TileCursor cursor = layer.tiles(b, 400, 300)
        assertEquals 4, cursor.z
        assertEquals 2, cursor.minX
        assertEquals 9, cursor.minY
        assertEquals 5, cursor.maxX
        assertEquals 10, cursor.maxY
        assertEquals 4, cursor.width
        assertEquals 2, cursor.height
        assertEquals 8, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 4, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 8, c
        layer.close()
    }

    @Test
    void getTileCoordinates() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        Map coords = layer.getTileCoordinates(b, layer.pyramid.grid(4))
        assertEquals 2, coords.minX
        assertEquals 9, coords.minY
        assertEquals 5, coords.maxX
        assertEquals 10, coords.maxY
        layer.close()
    }

    @Test
    void boundsAroundPoint() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        TileCursor cursor = layer.tiles(Projection.transform(new Point(-100.81,46.81),"EPSG:4326","EPSG:3857"), 8, 400, 300)
        assertEquals(8, cursor.z)
        assertEquals(55, cursor.minX)
        assertEquals(165, cursor.minY)
        assertEquals(57, cursor.maxX)
        assertEquals(166, cursor.maxY)
        assertEquals(3, cursor.width)
        assertEquals(2, cursor.height)
        assertEquals(6, cursor.size)
        layer.close()
    }

    @Test
    void getRaster() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        Raster raster = layer.getRaster(layer.tiles(b, 4))
        assertNotNull raster
        ImageAssert.assertEquals(new File(getClass().getClassLoader().getResource("geoscript/layer/tilelayer_raster.png").toURI()), raster.image, 100)
        File out = new File(folder, "raster.png")
        WorldImage format = new WorldImage(out)
        format.write(raster)
        assertTrue out.exists()
        assertTrue out.length() > 0
        layer.close()
    }

    @Test
    void getRasterCropped() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        Raster raster = layer.getRaster(b, 400, 300)
        assertNotNull raster
        ImageAssert.assertEquals(new File(getClass().getClassLoader().getResource("geoscript/layer/tilelayer_raster_cropped.png").toURI()), raster.image, 100)
        File out = new File(folder, "raster.png")
        WorldImage format = new WorldImage(out)
        format.write(raster)
        assertTrue out.exists()
        assertTrue out.length() > 0
        layer.close()
    }

    @Test
    void getRasterAroundPoint() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Point point = Projection.transform(new Point(-100.81,46.81),"EPSG:4326","EPSG:3857")
        Raster raster = layer.getRaster(point, 4, 300, 200)
        assertNotNull raster
        ImageAssert.assertEquals(new File(getClass().getClassLoader().getResource("geoscript/layer/tilelayer_raster_point.png").toURI()), raster.image, 100)
        File out = new File(folder, "raster.png")
        WorldImage format = new WorldImage(out)
        format.write(raster)
        assertTrue out.exists()
        assertTrue out.length() > 0
        layer.close()
    }

    @Test
    void getLayer() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        MBTiles layer = new MBTiles(file)
        Layer vlayer = layer.getLayer(layer.tiles(1))
        assertNotNull vlayer
        assertTrue vlayer.schema.has("the_geom")
        assertTrue vlayer.schema.has("id")
        assertTrue vlayer.schema.has("z")
        assertTrue vlayer.schema.has("x")
        assertTrue vlayer.schema.has("y")
        assertEquals 4, vlayer.count
        vlayer.eachFeature { Feature f ->
            assertTrue f['id'] in [0, 1, 2, 3]
            assertTrue f['z'] == 1
            assertTrue f['x'] in [0, 1]
            assertTrue f['y'] in [0, 1]
            assertNotNull f.geom
        }
    }

    @Test
    void withTileLayer() {
        File file = new File(getClass().getClassLoader().getResource("states.mbtiles").toURI())
        TileLayer.withTileLayer(new MBTiles(file)) { TileLayer layer ->
            Tile tile = layer.get(4, 2, 3)
            assertNotNull tile
            assertEquals 4, tile.z
            assertEquals 2, tile.x
            assertEquals 3, tile.y
            assertNotNull tile.data
        }
    }

    @Test
    void getTileLayerFromString() {
        // MBTiles params
        File file = new File(folder, 'test48.mbtiles')
        file.delete()
        TileLayer tileLayer = TileLayer.getTileLayer("type=mbtiles file=${file.absolutePath}")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof MBTiles)
        // MBTiles file
        file = new File(folder, 'test10.mbtiles')
        file.delete()
        tileLayer = TileLayer.getTileLayer(file.absolutePath)
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof MBTiles)
        // GeoPackage params
        file = new File(folder, 'test69.gpkg')
        file.delete()
        tileLayer = TileLayer.getTileLayer("type=geopackage file=${file.absolutePath}")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof GeoPackage)
        // GeoPackage file
        file = new File(folder, 'test19.gpkg')
        file.delete()
        tileLayer = TileLayer.getTileLayer(file.absolutePath)
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof GeoPackage)
        // TMS
        file = FileUtil.createDir(folder,"tms")
        tileLayer = TileLayer.getTileLayer("type=tms file=${file.absolutePath} format=jpeg")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof TMS)
        assertEquals(file.absolutePath, (tileLayer as TMS).dir.absolutePath)
        assertEquals("jpeg", (tileLayer as TMS).imageType)
        // OSM with URL
        tileLayer = TileLayer.getTileLayer("type=osm url=http://a.tile.openstreetmap.org")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof OSM)
        assertTrue((tileLayer as OSM).baseUrls.contains("http://a.tile.openstreetmap.org"))
        assertFalse((tileLayer as OSM).baseUrls.contains("http://b.tile.openstreetmap.org"))
        assertFalse((tileLayer as OSM).baseUrls.contains("http://c.tile.openstreetmap.org"))
        // OSM with URLs
        tileLayer = TileLayer.getTileLayer("type=osm urls=http://a.tile.openstreetmap.org,http://c.tile.openstreetmap.org")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof OSM)
        assertTrue((tileLayer as OSM).baseUrls.contains("http://a.tile.openstreetmap.org"))
        assertFalse((tileLayer as OSM).baseUrls.contains("http://b.tile.openstreetmap.org"))
        assertTrue((tileLayer as OSM).baseUrls.contains("http://c.tile.openstreetmap.org"))
        // OSM with no URLs
        tileLayer = TileLayer.getTileLayer("type=osm")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof OSM)
        assertTrue((tileLayer as OSM).baseUrls.contains("http://a.tile.openstreetmap.org"))
        assertTrue((tileLayer as OSM).baseUrls.contains("http://b.tile.openstreetmap.org"))
        assertTrue((tileLayer as OSM).baseUrls.contains("http://c.tile.openstreetmap.org"))
        // OSM
        tileLayer = TileLayer.getTileLayer("osm")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof OSM)
        assertTrue((tileLayer as OSM).baseUrls.contains("http://a.tile.openstreetmap.org"))
        assertTrue((tileLayer as OSM).baseUrls.contains("http://b.tile.openstreetmap.org"))
        assertTrue((tileLayer as OSM).baseUrls.contains("http://c.tile.openstreetmap.org"))
        // UTFGrid
        file = FileUtil.createDir(folder,"utfgrid")
        tileLayer = TileLayer.getTileLayer("type=utfgrid file=${file.absolutePath}")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof UTFGrid)
        // VectorTiles Directory
        file = FileUtil.createDir(folder,"vectortiles")
        tileLayer = TileLayer.getTileLayer("type=vectortiles file=${file.absolutePath} format=mvt pyramid=GlobalMercator")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof VectorTiles)
        assertEquals("mvt", (tileLayer as VectorTiles).type)
        assertEquals(file.absolutePath, (tileLayer as VectorTiles).dir.absolutePath)
        assertEquals(Pyramid.createGlobalMercatorPyramid(), (tileLayer as VectorTiles).pyramid)
        // VectorTiles URL
        tileLayer = TileLayer.getTileLayer("type=vectortiles url=http://vectortiles.org format=pbf pyramid=GlobalGeodetic")
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof VectorTiles)
        assertEquals("pbf", (tileLayer as VectorTiles).type)
        assertEquals("http://vectortiles.org", (tileLayer as VectorTiles).url.toString())
        assertEquals(Pyramid.createGlobalGeodeticPyramid(), (tileLayer as VectorTiles).pyramid)
        // Null
        tileLayer = TileLayer.getTileLayer("type=asdfasd")
        assertNull(tileLayer)
    }

    @Test
    void getTileLayerFromParams() {
        // MBTiles (file doesn't exist)
        File file = new File(folder, 'test45.mbtiles')
        file.delete()
        TileLayer tileLayer = TileLayer.getTileLayer([type: 'mbtiles', file: file])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof MBTiles)
        // MBTiles (empty file)
        file = new File(folder, 'test23.mbtiles')
        tileLayer = TileLayer.getTileLayer([type: 'mbtiles', file: file])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof MBTiles)
        // GeoPackage (file doesn't exist)
        file = new File(folder, 'test67.gpkg')
        file.delete()
        tileLayer = TileLayer.getTileLayer([type: 'geopackage', file: file])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof GeoPackage)
        // GeoPackage (empty file)
        file = new File(folder, 'test34.gpkg')
        file.delete()
        tileLayer = TileLayer.getTileLayer([type: 'geopackage', file: file])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof GeoPackage)
        // TMS
        file = FileUtil.createDir(folder,"tms")
        tileLayer = TileLayer.getTileLayer([type: 'tms', file: file, format: 'jpeg'])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof TMS)
        assertEquals(file.absolutePath, (tileLayer as TMS).dir.absolutePath)
        assertEquals("jpeg", (tileLayer as TMS).imageType)
        // OSM
        tileLayer = TileLayer.getTileLayer([type: 'osm', url: 'http://a.tile.openstreetmap.org'])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof OSM)
        assertTrue((tileLayer as OSM).baseUrls.contains("http://a.tile.openstreetmap.org"))
        // UTFGrid
        file = FileUtil.createDir(folder,"utfgrid")
        tileLayer = TileLayer.getTileLayer([type: 'utfgrid', file: file])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof UTFGrid)
        // VectorTiles Directory
        file = FileUtil.createDir(folder,"vectortiles")
        tileLayer = TileLayer.getTileLayer([type: 'vectortiles', file: file, format: 'mvt', pyramid: 'GlobalMercator'])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof VectorTiles)
        assertEquals("mvt", (tileLayer as VectorTiles).type)
        assertEquals(file.absolutePath, (tileLayer as VectorTiles).dir.absolutePath)
        assertEquals(Pyramid.createGlobalMercatorPyramid(), (tileLayer as VectorTiles).pyramid)
        // VectorTiles URL
        tileLayer = TileLayer.getTileLayer([type:'vectortiles', url: 'http://vectortiles.org', format: 'pbf', pyramid: Pyramid.createGlobalGeodeticPyramid()])
        assertNotNull(tileLayer)
        assertTrue(tileLayer instanceof VectorTiles)
        assertEquals("pbf", (tileLayer as VectorTiles).type)
        assertEquals("http://vectortiles.org", (tileLayer as VectorTiles).url.toString())
        assertEquals(Pyramid.createGlobalGeodeticPyramid(), (tileLayer as VectorTiles).pyramid)
    }

    @Test void getTileRenderer() {
        TileRenderer tileRenderer
        TileLayer tileLayer
        File file
        Layer layer = new Shapefile(new File(getClass().getClassLoader().getResource("states.mbtiles").toURI()))
        // MBTiles
        file = new File(folder, 'test1.mbtiles')
        file.delete()
        tileLayer = TileLayer.getTileLayer("type=mbtiles file=${file.absolutePath}")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNotNull(tileRenderer)
        assertTrue(tileRenderer instanceof ImageTileRenderer)
        // GeoPackage params
        file = new File(folder, 'test1.gpkg')
        file.delete()
        tileLayer = TileLayer.getTileLayer("type=geopackage file=${file.absolutePath}")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNotNull(tileRenderer)
        assertTrue(tileRenderer instanceof ImageTileRenderer)
        // GeoPackage file
        file = new File(folder, 'test2.gpkg')
        file.delete()
        tileLayer = TileLayer.getTileLayer(file.absolutePath)
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNotNull(tileRenderer)
        assertTrue(tileRenderer instanceof ImageTileRenderer)
        // TMS
        file = FileUtil.createDir(folder,"tms")
        tileLayer = TileLayer.getTileLayer("type=tms file=${file.absolutePath} format=jpeg")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNotNull(tileRenderer)
        assertTrue(tileRenderer instanceof ImageTileRenderer)
        // OSM
        tileLayer = TileLayer.getTileLayer("type=osm url=http://a.tile.openstreetmap.org")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNull(tileRenderer)
        // UTFGrid
        file = FileUtil.createDir(folder,"utfgrid")
        tileLayer = TileLayer.getTileLayer("type=utfgrid file=${file.absolutePath}")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNotNull(tileRenderer)
        assertTrue(tileRenderer instanceof UTFGridTileRenderer)
        // VectorTiles Directory
        file = FileUtil.createDir(folder,"vectortiles")
        tileLayer = TileLayer.getTileLayer("type=vectortiles file=${file.absolutePath} format=mvt pyramid=GlobalMercator")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNotNull(tileRenderer)
        assertTrue(tileRenderer instanceof VectorTileRenderer)
        // VectorTiles URL
        tileLayer = TileLayer.getTileLayer("type=vectortiles url=http://vectortiles.org format=pbf pyramid=GlobalGeodetic")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNotNull(tileRenderer)
        assertTrue(tileRenderer instanceof PbfVectorTileRenderer)
        // Null
        tileLayer = TileLayer.getTileLayer("type=asdfasd")
        tileRenderer = TileLayer.getTileRenderer(tileLayer, layer)
        assertNull(tileRenderer)
    }

}


<|start_filename|>src/test/groovy/geoscript/style/io/SLDReaderTest.groovy<|end_filename|>
package geoscript.style.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.style.Style

/**
 * The SLDReader UnitTest
 * @author <NAME>
 */
class SLDReaderTest {

    @Test void readFromFile() {

        File file = new File(getClass().getClassLoader().getResource("states.sld").toURI())
        assertNotNull(file)

        SLDReader reader = new SLDReader()
        Style style = reader.read(file)
        assertNotNull(style)
        assertNotNull(style.gtStyle)
    }

    @Test void readFromInputStream() {

        InputStream inputStream = getClass().getClassLoader().getResourceAsStream("states.sld")
        assertNotNull(inputStream)

        SLDReader reader = new SLDReader()
        Style style = reader.read(inputStream)
        assertNotNull(style)
        assertNotNull(style.gtStyle)
    }

    @Test void readFromString() {

        String sld = """<?xml version="1.0" encoding="UTF-8"?>
            <sld:StyledLayerDescriptor xmlns="http://www.opengis.net/sld" xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" version="1.0.0">
                <sld:UserLayer>
                    <sld:LayerFeatureConstraints>
                        <sld:FeatureTypeConstraint/>
                    </sld:LayerFeatureConstraints>
                    <sld:UserStyle>
                        <sld:Name>Default Styler</sld:Name>
                        <sld:Title/>
                        <sld:IsDefault>true</sld:IsDefault>
                        <sld:FeatureTypeStyle>
                            <sld:Name>simple</sld:Name>
                            <sld:FeatureTypeName>Feature</sld:FeatureTypeName>
                            <sld:SemanticTypeIdentifier>generic:geometry</sld:SemanticTypeIdentifier>
                            <sld:SemanticTypeIdentifier>simple</sld:SemanticTypeIdentifier>
                            <sld:Rule>
                                <sld:MaxScaleDenominator>1.7976931348623157E308</sld:MaxScaleDenominator>
                                <sld:PolygonSymbolizer>
                                    <sld:Fill>
                                        <sld:CssParameter name="fill">
                                            <ogc:Literal>#E6E6E6</ogc:Literal>
                                        </sld:CssParameter>
                                        <sld:CssParameter name="fill-opacity">
                                            <ogc:Literal>0.5</ogc:Literal>
                                        </sld:CssParameter>
                                    </sld:Fill>
                                    <sld:Stroke>
                                        <sld:CssParameter name="stroke">
                                            <ogc:Literal>#4C4C4C</ogc:Literal>
                                        </sld:CssParameter>
                                        <sld:CssParameter name="stroke-linecap">
                                            <ogc:Literal>butt</ogc:Literal>
                                        </sld:CssParameter>
                                        <sld:CssParameter name="stroke-linejoin">
                                            <ogc:Literal>miter</ogc:Literal>
                                        </sld:CssParameter>
                                        <sld:CssParameter name="stroke-opacity">
                                            <ogc:Literal>1.0</ogc:Literal>
                                        </sld:CssParameter>
                                        <sld:CssParameter name="stroke-width">
                                            <ogc:Literal>0.0</ogc:Literal>
                                        </sld:CssParameter>
                                        <sld:CssParameter name="stroke-dashoffset">
                                            <ogc:Literal>0.0</ogc:Literal>
                                        </sld:CssParameter>
                                    </sld:Stroke>
                                </sld:PolygonSymbolizer>
                            </sld:Rule>
                        </sld:FeatureTypeStyle>
                    </sld:UserStyle>
                </sld:UserLayer>
            </sld:StyledLayerDescriptor>
        """

        SLDReader reader = new SLDReader()
        Style style = reader.read(sld)
        assertNotNull(style)
        assertNotNull(style.gtStyle)
    }

}


<|start_filename|>src/main/groovy/geoscript/geom/PreparedGeometry.groovy<|end_filename|>
package geoscript.geom

import org.locationtech.jts.geom.prep.PreparedGeometry as JtsPreparedGeometry
import org.locationtech.jts.geom.prep.PreparedGeometryFactory

/**
 * A PreparedGeometry makes repeated spatial operations more efficient.
 * <p>You can create a PreparedGeometry by wrapping an existing {@link Geometry}:</p>
 * <p><blockquote><pre>
 * def p1 = new PreparedGeometry(new Point(1,4))
 * </pre></blockquote></p>
 * <p>Or by calling the prepare() method on a {@link Geometry}:<p>
 * <p><blockquote><pre>
 * def prep = Geometry.fromWKT('POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))').prepare()
 * </pre></blockquote></p>
 * <p>Or by using the Geometry.prepare() static method:</p>
 * <p><blockquote><pre>
 * def prep = Geometry.prepared(Geometry.fromWKT('POLYGON ((0 0, 5 0, 5 5, 0 5, 0 0))'))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class PreparedGeometry {

    /**
     * The wrapped JTS PreparedGeometry
     */
    JtsPreparedGeometry prepGeom

    /**
     * The JTS PreparedGeometryFactory
     */
    static PreparedGeometryFactory factory = new PreparedGeometryFactory()
    
    /**
     * Create a PreparedGeometry from a Geometry
     * @param A Geometry
     */
    PreparedGeometry(Geometry geom) {
        prepGeom = factory.create(geom.g)
    }

    /**
     * Whether this PreparedGeometry contains the given Geometry
     * @param The other Geometry
     * @return Whether this PreparedGeometry contains the other Geometry
     */
    boolean contains(Geometry geom) {
        prepGeom.contains(geom.g)
    }

    /**
     * Whether this PreparedGeometry contains the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry contains the given Geometry.
     */
    boolean containsProperly(Geometry geom) {
        prepGeom.containsProperly(geom.g)
    }

    /**
     * Whether this PreparedGeometry is covered by the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry is covered by the given Geometry.
     */
    boolean coveredBy(Geometry geom) {
        prepGeom.coveredBy(geom.g)
    }

    /**
     * Whether this PreparedGeometry covers the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry covers the given Geometry.
     */
    boolean covers(Geometry geom) {
        prepGeom.covers(geom.g)
    }

    /**
     * Whether this PreparedGeometry crosses the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry crosses the given Geometry.
     */
    boolean crosses(Geometry geom) {
        prepGeom.crosses(geom.g)
    }

    /**
     * Whether this PreparedGeometry is disjoint the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry is disjoint the given Geometry.
     */
    boolean disjoint(Geometry geom) {
        prepGeom.disjoint(geom.g)
    }

    /**
     * Get the base Geometry of this PreparedGeometry
     * @return The Geometry
     */
    Geometry getGeometry() {
        Geometry.wrap(prepGeom.geometry)
    }

    /**
     * Whether this PreparedGeometry intersects the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry intersects the given Geometry.
     */
    boolean intersects(Geometry geom) {
        prepGeom.intersects(geom.g)
    }

    /**
     * Whether this PreparedGeometry overlaps the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry overlaps the given Geometry.
     */
    boolean overlaps(Geometry geom) {
        prepGeom.overlaps(geom.g)
    }

    /**
     * Whether this PreparedGeometry touches the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry touches the given Geometry.
     */
    boolean touches(Geometry geom) {
        prepGeom.touches(geom.g)
    }

    /**
     * Whether this PreparedGeometry is within the given Geometry.
     * @param geom The other Geometry
     * @return Whether this PreparedGeometry is within the given Geometry.
     */
    boolean within(Geometry geom) {
        prepGeom.within(geom.g)
    }

    /**
     * The String representation
     * @return The String representation
     */
    String toString() {
        geometry.toString()
    }
}



<|start_filename|>src/main/groovy/geoscript/carto/PageSize.groovy<|end_filename|>
package geoscript.carto

/**
 * The PageSize controls the width and height of the cartographic document.
 * @author <NAME>
 */
class PageSize {

    /**
     * The letter landscape 8.5 x 11 page size
     */
    static LETTER_LANDSCAPE = new PageSize(792, 612)
    
    /**
     * The tabloid landscape 11 x 17 page size
     */
    static TABLOID_LANDSCAPE = new PageSize(1224, 792)

    /**
     * The C sized landscape page size
     */
    static C_LANDSCAPE = new PageSize(1584, 1224)

    /**
     * The D sized landscape page size
     */
    static D_LANDSCAPE = new PageSize(2448, 1584)

    /**
     * The E sized landscape page size
     */
    static E_LANDSCAPE = new PageSize(3168, 2448)

    /**
     * The letter portrait 11 x 8.5 page size
     */
    static LETTER_PORTRAIT = new PageSize(612, 792)

    /**
     * The tabloid portraait 17 x 11 page size
     */
    static TABLOID_PORTRAIT = new PageSize(792, 1224)

    /**
     * The C sized portrait page size
     */
    static C_PORTRAIT = new PageSize(1224, 1584)

    /**
     * The D sized portrait page size
     */
    static D_PORTRAIT = new PageSize(1584, 2448)

    /**
     * The E sized portrait page size
     */
    static E_PORTRAIT = new PageSize(2448, 3168)

    final int width;

    final int height;

    /**
     * Create a new PageSize with a width and height
     * @param width The width
     * @param height The height
     */
    PageSize(int width, int height) {
        this.width = width
        this.height = height
    }

    @Override
    String toString() {
        "PageSize(${width}, ${height})"
    }

}

<|start_filename|>src/test/groovy/geoscript/style/CompositeTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.filter.Filter

/**
 * The Composite Unit Test
 * @author <NAME>
 */
class CompositeTest {

    @Test void constructors() {
        Composite composite = new Composite(new Fill("red"))
        assertEquals 1, composite.parts.size()

        composite = new Composite([new Fill("red"), new Stroke("blue")])
        assertEquals 2, composite.parts.size()
    }

    @Test void where() {
        Composite comp = new Fill("red") + new Stroke("blue")
        assertEquals Filter.PASS, comp.filter
        assertEquals Filter.PASS, comp.parts[0].filter
        assertEquals Filter.PASS, comp.parts[1].filter

        assertTrue comp.where("NAME = 'Washington'") instanceof Symbolizer
        assertEquals new Filter("NAME = 'Washington'"), comp.filter
        assertEquals new Filter("NAME = 'Washington'"), comp.parts[0].filter
        assertEquals new Filter("NAME = 'Washington'"), comp.parts[1].filter
    }

    @Test void range() {
        Composite comp = new Fill("red") + new Stroke("blue")
        assertEquals(-1, comp.scale.min, 0.1)
        assertEquals(-1, comp.scale.max, 0.1)
        assertEquals(-1, comp.parts[0].scale.min, 0.1)
        assertEquals(-1, comp.parts[0].scale.max, 0.1)
        assertEquals(-1, comp.parts[1].scale.min, 0.1)
        assertEquals(-1, comp.parts[1].scale.max, 0.1)

        assertTrue comp.range(100, 950) instanceof Symbolizer
        assertEquals(100, comp.scale.min, 0.1)
        assertEquals(950, comp.scale.max, 0.1)
        assertEquals(100, comp.parts[0].scale.min, 0.1)
        assertEquals(950, comp.parts[0].scale.max, 0.1)
        assertEquals(100, comp.parts[1].scale.min, 0.1)
        assertEquals(950, comp.parts[1].scale.max, 0.1)
    }

    @Test void zindex() {
        Composite comp = new Fill("red") + new Stroke("blue")
        assertEquals 0, comp.z
        assertEquals 0, comp.parts[0].z
        assertEquals 0, comp.parts[1].z

        assertTrue comp.zindex(5) instanceof Symbolizer
        assertEquals 5, comp.z
        assertEquals 5, comp.parts[0].z
        assertEquals 5, comp.parts[1].z
    }

    @Test void string() {
        Composite comp = new Fill("red") + new Stroke("blue")
        assertEquals "Composite (Fill(color = #ff0000, opacity = 1.0), Stroke(color = #0000ff, width = 1))", comp.toString()
    }

}


<|start_filename|>src/test/groovy/geoscript/filter/PropertyTest.groovy<|end_filename|>
package geoscript.filter

import geoscript.feature.Feature
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.feature.Field

/**
 * The Property UnitTest
 * @author <NAME>
 */
class PropertyTest {

    @Test void constructors() {
        // Create a Property from a String
        Property p = new Property("NAME")
        assertTrue p.expr instanceof org.opengis.filter.expression.PropertyName
        assertEquals "NAME", p.toString()

        // Create a Property from a Field
        p = new Property(new Field("NAME","String"))
        assertTrue p.expr instanceof org.opengis.filter.expression.PropertyName
        assertEquals "NAME", p.toString()
    }

    @Test void evaluate(){
        Feature f = new Feature([type:"house", price: 24.4], "building1")
        Property p = new Property("type")
        assertEquals "house", p.evaluate(f)
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/GraticuleTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.FileUtil
import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.geom.Bounds
import geoscript.geom.Point
import geoscript.workspace.Directory
import geoscript.workspace.Geobuf
import geoscript.workspace.H2
import geoscript.workspace.Workspace
import org.geotools.grid.GridElement
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import java.awt.Color

import static org.junit.jupiter.api.Assertions.*

/**
 * The Graticule Unit Test
 * @author <NAME>
 */
class GraticuleTest {

    @TempDir
    private File folder

    @Test void createSquaresToMemory() {
        Layer layer = Graticule.createSquares(new Bounds(110.0, -45.0, 160.0, -5.0, "EPSG:4326"), 10, -1)
        assertEquals 20, layer.count
        assertEquals "grid", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "Polygon", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 20
        }
    }

    @Test void createSquaresToShapefile() {
        File dir = FileUtil.createDir(folder, "squares")
        Workspace workspace = new Directory(dir)
        Layer layer = Graticule.createSquares(new Bounds(110.0, -45.0, 160.0, -5.0, "EPSG:4326"), 10, 1,
                workspace: workspace, layer: "squares")
        assertEquals 20, layer.count
        assertEquals "squares", layer.name
        assertTrue layer.schema.has("the_geom")
        assertEquals "MultiPolygon", layer.schema["the_geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 20
        }
    }

    @Test void createOvalsToShapefile() {
        File dir = FileUtil.createDir(folder, "ovals")
        Workspace workspace = new Directory(dir)
        Layer layer = Graticule.createOvals(new Bounds(110.0, -45.0, 160.0, -5.0, "EPSG:4326"), 10,
                workspace: workspace, layer: "ovals")
        assertEquals 20, layer.count
        assertEquals "ovals", layer.name
        assertTrue layer.schema.has("the_geom")
        assertEquals "MultiPolygon", layer.schema["the_geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 20
        }
    }

    @Test void createHexagonsToMemory() {
        Layer layer = Graticule.createHexagons(new Bounds(0, 0, 100, 100), 5.0, -1, "flat")
        assertEquals 143, layer.count
        assertEquals "grid", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "Polygon", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 143
        }
    }

    @Test void createHexagonsToProperty() {
        File file = FileUtil.createDir(folder, "graticule.properties")
        Workspace workspace = new geoscript.workspace.Property(file)
        Layer layer = Graticule.createHexagons(new Bounds(0, 0, 100, 100), 5.0, 1, "angled",
                workspace: workspace, layer: "hexagons")
        assertEquals 143, layer.count
        assertEquals "hexagons", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "Polygon", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 143
        }
    }

    @Test void createHexagonsWithCustomSchema() {
        Schema schema = new Schema("hexagon", [
                new Field("geom", "Polygon"),
                new Field("color", "java.awt.Color")
        ])
        Bounds b = new Bounds(0,0,100,100)
        Layer layer = Graticule.createHexagons(b, 5.0, -1.0, "flat", schema: schema, setAttributes: { GridElement e, Map attributes ->
            int green = (255 * e.center.x / b.width)  as int
            int blue  = (255 * e.center.y / b.height) as int
            attributes["color"] = new Color(0, green, blue)
        })
        assertEquals 143, layer.count
        assertEquals "hexagon", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "Polygon", layer.schema["geom"].typ
        assertFalse layer.schema.has("id")
        assertTrue layer.schema.has("color")
        assertEquals "java.awt.Color", layer.schema["color"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertNotNull f["color"]
        }
    }

    @Test void createHexagonsOnlyIntersecting() {
        Layer states = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        Feature feature = states.first(filter: "STATE_NAME = 'Washington'")
        Layer layer = Graticule.createHexagons(feature.bounds.expandBy(1.0), 0.4, -1.0, "flat", createFeature: {GridElement e ->
            new Point(e.center.x, e.center.y).intersects(feature.geom)
        })
        assertEquals 45, layer.count
        assertEquals "grid", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "Polygon", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue feature.geom.intersects(f.geom)
            assertTrue f["id"] >= 0 && f["id"] <= 45
        }
    }

    @Test void createLines() {
        Layer layer = Graticule.createLines(new Bounds(110.0, -45.0, 160.0, -5.0, "EPSG:4326"), [
                [orientation: 'vertical',   level: 2, spacing: 10],
                [orientation: 'vertical',   level: 1, spacing: 2 ],
                [orientation: 'horizontal', level: 2, spacing: 10],
                [orientation: 'horizontal', level: 1, spacing: 2 ]
        ], 0.1)
        assertEquals 47, layer.count
        assertEquals "grid", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "LineString", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 200
        }
    }

    @Test void createLinesToH2() {
        File file = FileUtil.createDir(folder, "lines")
        Workspace workspace = new H2("lines", file)
        Layer layer = Graticule.createLines(new Bounds(110.0, -45.0, 160.0, -5.0, "EPSG:4326"), [
                [orientation: 'vertical',   level: 2, spacing: 10],
                [orientation: 'vertical',   level: 1, spacing: 2 ],
                [orientation: 'horizontal', level: 2, spacing: 10],
                [orientation: 'horizontal', level: 1, spacing: 2 ]
        ], 0.1, workspace: workspace, layer: "lines")
        assertEquals 47, layer.count
        assertEquals "lines", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "LineString", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 200
        }
    }

    @Test void createRectangles() {
        Layer layer = Graticule.createRectangles(new Bounds(0, 0, 100, 100), 10, 5, -1)
        assertEquals 200, layer.count
        assertEquals "grid", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "Polygon", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "Integer", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            assertFalse f.geom.empty
            assertTrue f["id"] >= 0 && f["id"] <= 200
        }
    }

    @Test void createRectanglesToGeobuf() {
        File file = FileUtil.createDir(folder, "rectangles")
        Workspace workspace = new Geobuf(file)
        Layer layer = Graticule.createRectangles(new Bounds(0, 0, 100, 100), 10, 5, -1,
                workspace: workspace, layer: "rectangles")
        assertEquals 200, layer.count
        assertEquals "rectangles", layer.name
        assertTrue layer.schema.has("geom")
        assertEquals "Geometry", layer.schema["geom"].typ
        assertTrue layer.schema.has("id")
        assertEquals "String", layer.schema["id"].typ
        layer.eachFeature { Feature f ->
            int id = f["id"] as int
            assertFalse f.geom.empty
            assertTrue id >= 0 && id <= 200
        }
    }
}


<|start_filename|>examples/raster_random.groovy<|end_filename|>
import geoscript.filter.Color
import geoscript.geom.Bounds
import geoscript.layer.Band
import geoscript.layer.GeoTIFF
import geoscript.layer.Raster

// Create a new Raster
Raster raster = new Raster(
        new Bounds(-180,-90,180,90,"EPSG:4326"),
        400,300,
        [
                new Band("red", 0, 255, 256),
                new Band("green", 0, 255, 256),
                new Band("blue", 0, 255, 256)
        ]
)
println "Bounds: ${raster.bounds}"
println "Size: ${raster.size[0]} x ${raster.size[1]}"

// Set values of each pixel
raster.eachCell { double value, double x, double y ->
    Color color = Color.randomPastel
    raster.setValue([x,y], color.rgb[0], 0)
    raster.setValue([x,y], color.rgb[1], 1)
    raster.setValue([x,y], color.rgb[2], 2)
}

// Write the Raster to disk
File file = new File("random.tif")
GeoTIFF geotiff = new GeoTIFF(file)
geotiff.write(raster)

<|start_filename|>src/main/groovy/geoscript/carto/LegendItem.groovy<|end_filename|>
package geoscript.carto

import geoscript.layer.Layer
import geoscript.layer.Raster
import geoscript.layer.Renderable
import geoscript.style.ColorMap
import geoscript.style.Style
import org.geotools.filter.text.cql2.CQL
import org.geotools.styling.Rule
import org.geotools.styling.StyleFactory
import org.geotools.styling.StyleFactoryImpl
import org.opengis.filter.Filter

import java.awt.Color
import java.awt.Font
import java.awt.Image

/**
 * Add a Legend to a cartographic document.
 * @author <NAME>
 */
class LegendItem extends Item {

    Color backgroundColor = Color.WHITE

    String title = "Legend"

    Font titleFont = new Font("Arial", Font.BOLD, 18)

    Color titleColor = Color.BLACK

    Font textFont = new Font("Arial", Font.PLAIN, 12)

    Color textColor = Color.BLACK

    List<LegendEntry> entries = []

    int legendEntryWidth = 50

    int legendEntryHeight = 30

    int gapBetweenEntries = 10

    String numberFormat = "#.##"

    /**
     * Create a Legend from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    LegendItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the background color
     * @param backgroundColor The background color
     * @return The LegendItem
     */
    LegendItem backgroundColor(Color backgroundColor) {
        this.backgroundColor = backgroundColor
        this
    }

    /**
     * Set the legend title
     * @param title The title
     * @return The LegendItem
     */
    LegendItem title(String title) {
        this.title = title
        this
    }

    /**
     * Set the title Font
     * @param titleFont The title Font
     * @return The LegendItem
     */
    LegendItem titleFont(Font titleFont) {
        this.titleFont = titleFont
        this
    }

    /**
     * Set the title Color
     * @param titleColor The title Color
     * @return The LegendItem
     */
    LegendItem titleColor(Color titleColor) {
        this.titleColor = titleColor
        this
    }

    /**
     * Set the text Font
     * @param textFont The text Font
     * @return The LegendItem
     */
    LegendItem textFont(Font textFont) {
        this.textFont = textFont
        this
    }

    /**
     * Set the text Color
     * @param textColor The text Color
     * @return The LegendItem
     */
    LegendItem textColor(Color textColor) {
        this.textColor = textColor
        this
    }

    /**
     * Set the legend entry width
     * @param legendEntryWidth The legend entry width
     * @return The LegendItem
     */
    LegendItem legendEntryWidth(int legendEntryWidth) {
        this.legendEntryWidth = legendEntryWidth
        this
    }

    /**
     * Set the legen entry height
     * @param legendEntryHeight The legend entry height
     * @return The LegendItem
     */
    LegendItem legendEntryHeight(int legendEntryHeight) {
        this.legendEntryHeight = legendEntryHeight
        this
    }

    /**
     * Set the gap between entries
     * @param gapBetweenEntries The gap between entries
     * @return The LegendItem
     */
    LegendItem gapBetweenEntries(int gapBetweenEntries) {
        this.gapBetweenEntries = gapBetweenEntries
        this
    }

    /**
     * Set the number format
     * @param numberFormat The number format
     * @return The LegendItem
     */
    LegendItem numberFormat(String numberFormat) {
        this.numberFormat = numberFormat
        this
    }

    /**
     * Add a Point legend entry
     * @param title The title
     * @param symbolizer The Point's Symbolizer
     * @return The LegendItem
     */
    LegendItem addPointEntry(String title, Style symbolizer) {
        this.entries.add(new LegendEntry(title: title, type: LegendEntryType.POINT, symbolizer: symbolizer))
        this
    }

    /**
     * Add a Line legend entry
     * @param title The title
     * @param symbolizer The Line's Symbolizer
     * @return The LegendItem
     */
    LegendItem addLineEntry(String title, Style symbolizer) {
        this.entries.add(new LegendEntry(title: title, type: LegendEntryType.LINE, symbolizer: symbolizer))
        this
    }

    /**
     * Add a Polygon legend entry
     * @param title The title
     * @param symbolizer The Polygon's Symbolizer
     * @return The LegendItem
     */
    LegendItem addPolygonEntry(String title, Style symbolizer) {
        this.entries.add(new LegendEntry(title: title, type: LegendEntryType.POLYGON, symbolizer: symbolizer))
        this
    }

    /**
     * Add a Group Entry
     * @param title The title
     * @return The LegendItem
     */
    LegendItem addGroupEntry(String title) {
        this.entries.add(new LegendEntry(title: title, type: LegendEntryType.GROUP))
        this
    }

    /**
     * Add a ColorMap legend entry
     * @param title The title
     * @param symbolizer The Raster's Symbolizer
     * @return The LegendItem
     */
    LegendItem addColorMapEntry(String title, ColorMap colorMap) {
        this.entries.add(new LegendEntry(title: title, type:LegendEntryType.COLORMAP, symbolizer: colorMap))
        this
    }

    /**
     * Add an Image Entry
     * @param title The title
     * @return The LegendItem
     */
    LegendItem addImageEntry(String title, Image image) {
        this.entries.add(new LegendEntry(title: title, type: LegendEntryType.IMAGE, image: image))
        this
    }

    /**
     * Add Legend entries for a Layer
     * @param layer A Layer
     * @return The LegendItem
     */
    LegendItem addLayer(Layer layer) {
        String geometryType = layer.schema.geom.typ.toLowerCase()
        LegendEntryType legendEntryType
        if (geometryType.contains("point")) {
            legendEntryType = LegendEntryType.POINT
        } else if (geometryType.contains("linestring")) {
            legendEntryType = LegendEntryType.LINE
        } else /*if (geometryType.contains("polygon") )*/ {
            legendEntryType = LegendEntryType.POLYGON
        }
        int numberOfRules = countRules(layer.style.gtStyle)
        if (numberOfRules > 1) {
            addGroupEntry(layer.name.capitalize())
        }
        layer.style.gtStyle.featureTypeStyles().each { def fts ->
            fts.rules().each { def rule ->
                Filter filter = rule.filter
                String title = numberOfRules > 1 ? CQL.toCQL(filter) : layer.name.capitalize()
                Style style = SLDStyle.fromRule(rule)
                if (legendEntryType == LegendEntryType.POINT) {
                    addPointEntry(title, style)
                } else if (legendEntryType == LegendEntryType.LINE) {
                    addLineEntry(title, style)
                }  else if (legendEntryType == LegendEntryType.POLYGON) {
                    addPolygonEntry(title, style)
                }
            }
        }
        this
    }

    /**
     * Add Legend entries for a Raster
     * @param raster The Raster
     * @return The LegendItem
     */
    LegendItem addRaster(Raster raster) {
        Style style = raster.style
        if (style instanceof ColorMap) {
            addColorMapEntry(title, style)
        } else {
            addImageEntry(raster.name.capitalize(), raster.resample(size: [legendEntryWidth, legendEntryHeight]).bufferedImage)
        }
        this
    }

    /**
     * Add Legend entries for a Map
     * @param map The Map
     * @return The LegendItem
     */
    LegendItem addMap(geoscript.render.Map map) {
        map.layers.each { Renderable renderable ->
            if (renderable instanceof Layer) {
                addLayer(renderable as Layer)
            } else if (renderable instanceof Raster) {
                addRaster(renderable as Raster)
            }
        }
        this
    }

    private int countRules(org.opengis.style.Style style) {
        int numberOfRules = 0
        style.featureTypeStyles().each { def fts ->
            fts.rules().each { def rule ->
                numberOfRules++
            }
        }
        numberOfRules
    }

    private static class SLDStyle implements Style {
        private final org.geotools.styling.Style style
        private final static StyleFactory styleFactory = new StyleFactoryImpl()
        SLDStyle(org.geotools.styling.Style style) {
            this.style = style
        }
        org.geotools.styling.Style getGtStyle() {
            style
        }
        static SLDStyle fromRule(Rule rule) {
            def style = styleFactory.createStyle()
            def fts = styleFactory.createFeatureTypeStyle()
            def ruleWithoutFilter = styleFactory.createRule()
            ruleWithoutFilter.symbolizers().addAll(rule.symbolizers())
            fts.rules().add(ruleWithoutFilter)
            style.featureTypeStyles().add(fts)
            new SLDStyle(style)
        }
    }

    @Override
    String toString() {
        "LegendItem(" +
            "x = " + x +
            ", y = " + y +
            ", width = " + width +
            ", height = " + height +
            ", backgroundColor = " + backgroundColor +
            ", title = " + title + '\'' +
            ", titleFont = " + titleFont +
            ", titleColor = " + titleColor +
            ", textFont = " + textFont +
            ", textColor = " + textColor +
            ", entries = " + entries +
            ", legendEntryWidth = " + legendEntryWidth +
            ", legendEntryHeight = " + legendEntryHeight +
            ", gapBetweenEntries = " + gapBetweenEntries +
            ", numberFormat = " + numberFormat +
            ')';
    }

    protected static class LegendEntry {
        String title
        LegendEntryType type
        Style symbolizer
        Image image
    }

    protected enum LegendEntryType {
        GROUP,
        POINT,
        LINE,
        POLYGON,
        COLORMAP,
        IMAGE
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/CartoBuilder.groovy<|end_filename|>
package geoscript.carto

/**
 * Create a cartographic map
 * @author <NAME>
 */
interface CartoBuilder {

    /**
     * Add a date
     * @param dateTextItem The DateTextItem
     * @return The CartoBuilder
     */
    CartoBuilder dateText(DateTextItem dateTextItem)

    /**
     * Add an image
     * @param imageItem The ImageItem
     * @return The CartoBuilder
     */
    CartoBuilder image(ImageItem imageItem)

    /**
     * Add a line
     * @param lineItem The LineItem
     * @return The CartoBuilder
     */
    CartoBuilder line(LineItem lineItem)

    /**
     * Add a Map
     * @param mapItem The MapItem
     * @return The CartoBuilder
     */
    CartoBuilder map(MapItem mapItem)

    /**
     * Add an overview Map
     * @param overviewMapItem The OverviewMapItem
     * @return The CartoBuilder
     */
    CartoBuilder overViewMap(OverviewMapItem overviewMapItem)

    /**
     * Add a north arrow
     * @param northArrowItem The NorthArrowItem
     * @return The CartoBuilder
     */
    CartoBuilder northArrow(NorthArrowItem northArrowItem)

    /**
     * Add text
     * @param textItem The TextItem
     * @return The CartoBuilder
     */
    CartoBuilder text(TextItem textItem)

    /**
     * Add a paragraph
     * @param paragraphItem The ParagraphItem
     * @return The CartoBuilder
     */
    CartoBuilder paragraph(ParagraphItem paragraphItem)

    /**
     * Add a rectangle
     * @param rectangleItem The RectangleItem
     * @return The CartoBuilder
     */
    CartoBuilder rectangle(RectangleItem rectangleItem)

    /**
     * Add scale text
     * @param scaleTextItem The ScaleTextItem
     * @return The CartoBuilder
     */
    CartoBuilder scaleText(ScaleTextItem scaleTextItem)

    /**
     * Add scale bar
     * @param scaleBarItem The ScaleBarItem
     * @return The CartoBuilder
     */
    CartoBuilder scaleBar(ScaleBarItem scaleBarItem)

    /**
     * Add a grid (usually for visually placing other items)
     * @param gridItem The GridItem
     * @return The CartoBuilder
     */
    CartoBuilder grid(GridItem gridItem)

    /**
     * Add a table
     * @param tableItem The TableItem
     * @return The CartoBuilder
     */
    CartoBuilder table(TableItem tableItem)

    /**
     * Add a legend
     * @param legendItem The LegendItem
     * @return The CartoBuilder
     */
    CartoBuilder legend(LegendItem legendItem)

    /**
     * Write the cartographic document to the OutputStream
     * @param outputStream The OutputStream
     */
    void build(OutputStream outputStream)

}



<|start_filename|>src/test/groovy/geoscript/geom/io/GeoJSONWriterTest.groovy<|end_filename|>
package geoscript.geom.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import geoscript.geom.*

/**
 * The GeoJSONWriter Unit Test
 * @author <NAME>
 */
class GeoJSONWriterTest {

    @Test void writePointPrettyPrint() {
        GeoJSONWriter writer = new GeoJSONWriter()
        Point p = new Point(111.1,-47.2)
        assertEquals """{
    "type": "Point",
    "coordinates": [
        111.1,
        -47.2
    ]
}""", writer.write(p, prettyPrint: true)
        assertEquals """{"type":"Point","coordinates":[111.1,-47.2]}""", writer.write(p, prettyPrint: false)
    }

    @Test void writePoint() {
        GeoJSONWriter writer = new GeoJSONWriter()
        Point p = new Point(111.1,-47.2)
        assertEquals """{"type":"Point","coordinates":[111.1,-47.2]}""", writer.write(p)
    }

    @Test void writePointWithDecimals() {
        GeoJSONWriter writer = new GeoJSONWriter()
        Point p = new Point(111.123456,-47.234567)
        assertEquals """{"type":"Point","coordinates":[111.123456,-47.234567]}""", writer.write(p, decimals: 6)
    }

    @Test void writeLineString() {
        GeoJSONWriter writer = new GeoJSONWriter()
        LineString l = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        assertEquals """{"type":"LineString","coordinates":[[111,-47],[123,-48],[110,-47]]}""", writer.write(l)
    }

    @Test void writeLinearRing() {
        GeoJSONWriter writer = new GeoJSONWriter()
        LinearRing l = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        assertEquals """{"type":"LineString","coordinates":[[111,-47],[123,-48],[110,-47],[111,-47]]}""", writer.write(l)
    }

    @Test void writePolygon() {
        GeoJSONWriter writer = new GeoJSONWriter()
        Polygon p = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        String expected = """{"type":"Polygon","coordinates":[[[1,1],[10,1],[10,10],[1,10],[1,1]],[[2,2],[4,2],[4,4],[2,4],[2,2]],[[5,5],[6,5],[6,6],[5,6],[5,5]]]}"""
        String actual = writer.write(p)
        assertEquals expected, actual
    }

    @Test void writeMultiPoint() {
        GeoJSONWriter writer = new GeoJSONWriter()
        MultiPoint p = new MultiPoint([111,-47],[110,-46.5])
        String expected = """{"type":"MultiPoint","coordinates":[[111,-47],[110,-46.5]]}"""
        String actual = writer.write(p)
        assertEquals expected, actual
    }

    @Test void writeMultiLineString() {
        GeoJSONWriter writer = new GeoJSONWriter()
        MultiLineString m = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        String expected = """{"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]}"""
        String actual = writer.write(m)
        assertEquals expected, actual
    }

    @Test void writeMultiPolygon() {
        GeoJSONWriter writer = new GeoJSONWriter()
        MultiPolygon mp = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        String expected = """{"type":"MultiPolygon","coordinates":[[[[1,2],[3,4],[5,6],[1,2]]],[[[7,8],[9,10],[11,12],[7,8]]]]}"""
        String actual = writer.write(mp)
        assertEquals expected, actual
    }

    @Test void writeGeometryCollection() {
        GeoJSONWriter writer = new GeoJSONWriter()
        GeometryCollection gc = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        String expected = """{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[100,0.0]},{"type":"LineString","coordinates":[[101,0.0],[102,1]]}]}"""
        String actual = writer.write(gc)
        assertEquals expected, actual
    }

}



<|start_filename|>src/main/groovy/geoscript/workspace/SpatiaLite.groovy<|end_filename|>
package geoscript.workspace

import geoscript.feature.Schema
import geoscript.layer.Cursor
import geoscript.layer.Layer
import org.geotools.data.DataStore
import org.geotools.data.ogr.OGRDataStore

/**
 * A SpatiaLite Workspace connects to a SpatiaLite database.
 * <p><blockquote><pre>
 * SpatiaLite spatialite = new SpatiaLite("db.sqlite")
 * </pre></blockquote></p>
 * @author <NAME>
 */
class SpatiaLite extends OGR {

    SpatiaLite(File file) {
        this(file.absolutePath)
    }

    SpatiaLite(String fileName) {
        super("SQLite", fileName)
    }

    protected SpatiaLite(OGRDataStore dataStore) {
        super(dataStore)
    }

    @Override
    Layer create(Map options, Schema schema, boolean write) {
        (options.get("options",[]) as List).add("SPATIALITE=YES")
        super.create(options, schema, write)
    }

    @Override
    Layer create(Map options = [:], Cursor c) {
        (options.get("options",[]) as List).add("SPATIALITE=YES")
        super.create(options, c)
    }

    @Override
    Layer add(Map options, Layer layer) {
        (options.get("options",[]) as List).add("SPATIALITE=YES")
        super.add(options, layer)
    }

    @Override
    Layer add(Map options, Layer layer, String name, int chunk=1000) {
        (options.get("options",[]) as List).add("SPATIALITE=YES")
        super.add(options, layer, name, chunk)
    }

    /**
     * Get the format
     * @return The workspace format name
     */
    String getFormat() {
        return "SpatiaLite"
    }

    /**
     * The String representation
     * @return A String representation
     */
    @Override
    String toString() {
        "SpatiaLite(${dataset})"
    }

    /**
     * The SpatiaLite WorkspaceFactory
     */
    static class Factory extends WorkspaceFactory<SpatiaLite> {

        @Override
        Map getParametersFromString(String str) {
            Map params = [:]
            if (!str.contains("=") && (str.endsWith(".sqlite") || str.endsWith(".spatialite"))) {
                params.put("DriverName", "SQLite")
                params.put("DatasourceName", new File(str).absolutePath)
            } else {
                params = super.getParametersFromString(str)
            }
            params
        }

        @Override
        SpatiaLite create(String type, Map params) {
            if (type.equalsIgnoreCase('spatialite')) {
                params.put("DriverName", "SQLite")
                if (params.containsKey('file')) {
                    Object file = params["file"]
                    params.put("DatasourceName", file instanceof File ? file.absolutePath: new File(file.toString()).absolutePath)
                }
                if (params['database'] instanceof File) {
                    params['DatasourceName'] = (params['database'] as File).absolutePath
                }
                super.create(params)
            } else {
                null
            }
        }

        @Override
        SpatiaLite create(DataStore dataStore) {
            SpatiaLite spatialite = null
            if (dataStore instanceof org.geotools.data.ogr.OGRDataStore) {
                def ogrDataSource = dataStore as org.geotools.data.ogr.OGRDataStore
                if (ogrDataSource.@ogrDriver.equalsIgnoreCase("SQLite")) {
                    spatialite = new SpatiaLite(ogrDataSource)
                }
            }
            spatialite
        }
    }
}

<|start_filename|>src/main/groovy/geoscript/carto/NorthArrowItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Color
import java.awt.Font

/**
 * Add a north arrow to a cartographic document.
 * @author <NAME>
 */
class NorthArrowItem extends Item {

    Color fillColor1 = Color.BLACK

    Color strokeColor1 = Color.BLACK

    Color fillColor2 = Color.WHITE

    Color strokeColor2 = Color.BLACK

    float strokeWidth = 1

    boolean drawText = false

    Font font = new Font("Arial", Font.BOLD, 48)

    Color textColor = Color.BLACK

    NorthArrowStyle style = NorthArrowStyle.North

    /**
     * Create a north arrow from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    NorthArrowItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the NorthArrowStyle
     * @param northArrowStyle The NorthArrowStyle
     * @return The NorthArrowItem
     */
    NorthArrowItem style(NorthArrowStyle northArrowStyle) {
        this.style = northArrowStyle
        this
    }

    /**
     * Set the first stroke color
     * @param color The first stroke color
     * @return The NorthArrowItem
     */
    NorthArrowItem strokeColor1(Color color) {
        this.strokeColor1 = color
        this
    }

    /**
     * Set the first fill color
     * @param color The first fill color
     * @return The NorthArrowItem
     */
    NorthArrowItem fillColor1(Color color) {
        this.fillColor1 = color
        this
    }

    /**
     * Set the second stroke Color
     * @param color The second stroke Color
     * @return The NorthArrowItem
     */
    NorthArrowItem strokeColor2(Color color) {
        this.strokeColor2 = color
        this
    }

    /**
     * Set the second fill color
     * @param color The second fill Color
     * @return The NorthArrowItem
     */
    NorthArrowItem fillColor2(Color color) {
        this.fillColor2 = color
        this
    }

    /**
     * Set the stroke width
     * @param strokeWidth The stroke width
     * @return The NorthArrowItem
     */
    NorthArrowItem strokeWidth(float strokeWidth) {
        this.strokeWidth = strokeWidth
        this
    }

    /**
     * Set whether to draw text or not
     * @param drawText Whether to draw text or not
     * @return The NorthArrowItem
     */
    NorthArrowItem drawText(boolean drawText) {
        this.drawText = drawText
        this
    }

    /**
     * Set the Font
     * @param font The Font
     * @return The NorthArrowItem
     */
    NorthArrowItem font(Font font) {
        this.font = font
        this
    }

    /**
     * Set the text Color
     * @param textColor The text color
     * @return The NorthArrowItem
     */
    NorthArrowItem textColor(Color textColor) {
        this.textColor = textColor
        this
    }

    @Override
    String toString() {
        "NorthArrowItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, " +
                "fill-color1 = ${fillColor1}, stroke-color1 = ${strokeColor1}, " +
                "fill-color2 = ${fillColor2}, stroke-color2 = ${strokeColor2}, stroke-width = ${strokeWidth}, " +
                "draw-text = ${drawText}, font = ${font}, text-color = ${textColor}, style = ${style.toString()})"
    }

}


<|start_filename|>src/test/groovy/geoscript/style/GradientTest.groovy<|end_filename|>
package geoscript.style

import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.geom.Point
import geoscript.layer.Layer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

import geoscript.filter.Expression
import geoscript.render.Map
import geoscript.layer.Shapefile

/**
 * The Gradient UnitTest
 * @author <NAME>
 */
class GradientTest {

    @TempDir
    File folder

    @Test void gradientBetweenStyles() {

        Gradient gradient = new Gradient(
            new Expression("PERSONS / LAND_KM"),
            [0,200],
            [new Fill("#000066") + new Stroke("black",0.1), new Fill("red") + new Stroke("black",0.1)],
            10,
            "exponential"
        )
        assertEquals 11, gradient.parts.size()

        Composite composite = gradient + ((new Fill("red")  + new Stroke("black",0.1)).where("PERSONS / LAND_KM > 200"))

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shp = new Shapefile(file)
        shp.style = composite

        Map map = new Map()
        map.addLayer(shp)

        File imgFile = new File(folder,"states.png")
        map.render(imgFile)
    }

    @Test void gradientWithColorBrewerForPoints() {
        Schema s = new Schema("facilities", [new Field("geom","Point", "EPSG:4326"), new Field("name","string"), new Field("price","float")])
        Layer layer = new Layer("facilities", s)
        layer.add(new Feature([new Point(111,-47), "House 1", 12.5], "house1", s))
        layer.add(new Feature([new Point(112,-46), "House 2", 13.5], "house2", s))
        layer.add(new Feature([new Point(113,-45), "House 3", 14.5], "house3", s))

        Gradient gradient = new Gradient(layer, "price", "Quantile", 3, "Greens")
        layer.style = gradient

        Map map = new Map()
        map.addLayer(layer)

        File imgFile = new File(folder,"facilities.png")
        map.render(imgFile)
    }

    @Test void gradientForLayerAndField() {

        // Get states shapefile
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(file)

        Map map = new Map()
        map.addLayer(shapefile)

        Gradient sym1 = new Gradient(shapefile, "WORKERS", "Quantile", 5, "Greens")
        assertNotNull(sym1)
        assertEquals(5, sym1.parts.size())

        File imgFile1 = new File(folder,"states1.png")
        shapefile.style = sym1
        map.render(imgFile1)

        Gradient sym2 = new Gradient(shapefile, "WORKERS", "EqualInterval", 7, ["#eee8aa","#98fb98","#afeeee","#d87093","#ffefd5","#ffdab9","#cd853f"])
        assertNotNull(sym2)
        assertEquals(7, sym2.parts.size())

        File imgFile2 = new File(folder,"states2.png")
        shapefile.style = sym2
        map.render(imgFile2)
    }

}


<|start_filename|>src/test/groovy/geoscript/render/MapTest.groovy<|end_filename|>
package geoscript.render

import geoscript.geom.Bounds
import geoscript.layer.Shapefile
import geoscript.proj.Projection
import geoscript.layer.GeoTIFF
import geoscript.layer.Raster
import geoscript.style.Fill
import geoscript.style.Stroke
import org.geotools.image.test.ImageAssert
import geoscript.layer.MBTiles
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import javax.imageio.ImageIO

import static org.junit.jupiter.api.Assertions.*

/**
 * The Map UnitTest
 * @author <NAME>
 */
class MapTest {

    @TempDir
    File folder

    private File getFile(String resource) {
        return new File(getClass().getClassLoader().getResource(resource).toURI())
    }

    @Test void proj() {
        Map map = new Map();
        map.proj = new Projection("EPSG:2927")
        assertEquals("EPSG:2927", map.proj.id)
        map.proj = "EPSG:4326"
        assertEquals("EPSG:4326", map.proj.id)
    }

    @Test void layer() {
        Map map = new Map()
        assertEquals(0, map.layers.size())
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        map.addLayer(shp)
        assertEquals(1, map.layers.size())
        map.layers = [shp]
        assertEquals(1, map.layers.size())
        map.close()
    }

    @Test void renderToImage() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)

        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.proj = new Projection("EPSG:2927")
        map.addLayer(shp)
        map.bounds = shp.bounds
        def image = map.renderToImage()
        assertNotNull(image)

        File out = new File(folder,"map.png")
        javax.imageio.ImageIO.write(image, "png", out);
        assertTrue(out.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_to_image.png"), ImageIO.read(out), 200)
    }

    @Test void renderRasterToImage() {
        File file = new File(getClass().getClassLoader().getResource("alki.tif").toURI())
        assertNotNull(file)

        GeoTIFF geoTIFF = new GeoTIFF(file)
        Raster raster = geoTIFF.read()

        Map map = new Map()
        map.proj = new Projection("EPSG:2927")
        map.addRaster(raster)
        map.bounds = raster.bounds
        def image = map.renderToImage()
        assertNotNull(image)

        File out = new File(folder,"raster.png")
        javax.imageio.ImageIO.write(image, "png", out);
        assertTrue(out.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_to_raster.png"), ImageIO.read(out), 100)
    }

    @Test void renderDemRaster() {
        File file = new File(getClass().getClassLoader().getResource("raster.tif").toURI())
        assertNotNull(file)

        GeoTIFF geoTIFF = new GeoTIFF(file)
        Raster raster = geoTIFF.read()
        raster.style = new  geoscript.style.ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])

        Map map = new Map()
        map.addRaster(raster)
        def image = map.renderToImage()
        assertNotNull(image)

        File out = new File(folder,"raster.png")
        javax.imageio.ImageIO.write(image, "png", out);
        assertTrue(out.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_to_dem.png"), ImageIO.read(out), 100)
    }

    @Test void renderTileLayer() {
        MBTiles layer = new MBTiles(new File(getClass().getClassLoader().getResource("states.mbtiles").toURI()))
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        shp.style = new Stroke("#ff0000", 2.0)

        Map map = new Map()
        map.addTileLayer(layer)
        map.addLayer(shp)
        def image = map.renderToImage()
        assertNotNull(image)

        File out = new File(folder,"raster.png")
        javax.imageio.ImageIO.write(image, "png", out);
        assertTrue(out.exists())
        map.close()
    }

    @Test void renderToImageWithMapNoProjection() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)

        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.addLayer(shp)
        map.bounds = shp.bounds
        def image = map.renderToImage()
        assertNotNull(image)

        File out = new File(folder,"map.png")
        javax.imageio.ImageIO.write(image, "png", out);
        assertTrue(out.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_to_image_noproj.png"), ImageIO.read(out), 200)
    }

    @Test void renderToImageWithMapBoundsNoProjection() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)

        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.addLayer(shp)
        map.bounds = new Bounds(-126, 45.315, -116, 50.356)
        def image = map.renderToImage()
        assertNotNull(image)

        File out = new File(folder,"map.png")
        javax.imageio.ImageIO.write(image, "png", out);
        assertTrue(out.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_to_image_noproj_nobounds.png"), ImageIO.read(out), 200)
    }

    @Test void renderToFile() {
        File out = new File(folder,"map.png")
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.proj = new Projection("EPSG:2927")
        map.addLayer(shp)
        map.bounds = shp.bounds
        map.render(out)
        assertTrue(out.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_to_file.png"), ImageIO.read(out), 200)
    }

    @Test void renderToContinuousFile() {
        File out = new File(folder,"map.png")
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.width = 400
        map.height = 100
        map.proj = new Projection("EPSG:4326")
        map.addLayer(shp)
        map.bounds = new Bounds(-180, -90, 180, 90, "EPSG:4326")
        map.render(out)
        assertTrue(out.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_continuous.png"), ImageIO.read(out), 100)
    }

    @Test void renderToOutputStream() {
        File f = new File(folder,"map.png")
        FileOutputStream out = new FileOutputStream(f)

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.proj = new Projection("EPSG:2927")
        map.addLayer(shp)
        map.bounds = shp.bounds
        map.render(out)
        out.close()
        assertTrue(f.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map_to_out.png"), ImageIO.read(f), 200)
    }

    @Test void renderToPdf() {
        File f = new File(folder,"map.pdf")

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        shp.style = new Fill("white") + new Stroke("#CCCCCC", 0.1)

        Map map = new Map(type:"pdf", layers:[shp])
        map.addLayer(shp)
        map.render(f)
        assertTrue(f.exists())
        map.close()
    }

    @Test void renderToSvg() {
        File f = new File(folder,"map.svg")

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        shp.style = new Fill("white") + new Stroke("#CCCCCC", 0.1)

        Map map = new Map(type:"svg", layers:[shp])
        map.render(f)
        assertTrue(f.exists())
        map.close()
    }

    @Test void renderToJpeg() {
        File f = new File(folder,"map.jpeg")

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        shp.style = new Fill("white") + new Stroke("#CCCCCC", 0.1)

        Map map = new Map(type:"jpeg", layers: [shp])
        map.render(f)
        assertTrue(f.exists())
        map.close()

        ImageAssert.assertEquals(getFile("geoscript/render/map.jpeg"), ImageIO.read(f), 100)
    }

    @Test void renderToGif() {
        File f = new File(folder,"map.gif")

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        shp.style = new Fill("white") + new Stroke("#CCCCCC", 0.1)

        Map map = new Map(type:"gif", layers: [shp])
        map.render(f)
        assertTrue(f.exists())
        map.close()

        // File f2 = new File(folder,"map2.gif")
        // ImageIO.write(ImageIO.read(f), 'gif', f2)
        // ImageAssert.assertEquals(getFile("geoscript/render/map.gif"), ImageIO.read(f2), 100)
    }

    @Test void renderToBase64() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)
        shp.style = new Fill("white") + new Stroke("#CCCCCC", 0.1)

        Map map = new Map(type:"base64", layers: [shp])
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        map.render(out)
        map.close()
        byte[] bytes = out.toByteArray()
        assertTrue(bytes.length > 0)
    }

    @Test void getScaleDenominator() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.proj = new Projection("EPSG:2927")
        map.addLayer(shp)
        map.bounds = shp.bounds
        assertEquals(38273743.41534821, map.scaleDenominator, 0.01)
    }

    @Test void getBounds() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        assertNotNull(file)
        Shapefile shp = new Shapefile(file)
        assertNotNull(shp)

        Map map = new Map()
        map.proj = new Projection("EPSG:2927")
        map.addLayer(shp)
        map.bounds = shp.bounds
        assertEquals(shp.bounds.minX, map.bounds.minX, 0.01)
        assertEquals(shp.bounds.maxX, map.bounds.maxX, 0.01)
        assertEquals(shp.bounds.minY, map.bounds.minY, 0.01)
        assertEquals(shp.bounds.maxY, map.bounds.maxY, 0.01)
        assertEquals(shp.bounds.proj.id, map.bounds.proj.id)
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/ScaleTextItem.groovy<|end_filename|>
package geoscript.carto

import geoscript.render.Map

import java.awt.Color
import java.awt.Font

/**
 * Add scale text to a cartographic document.
 * @author <NAME>
 */
class ScaleTextItem extends Item {

    Map map

    Color color = Color.BLACK

    Font font = new Font("Arial", Font.PLAIN, 12)

    HorizontalAlign horizontalAlign = HorizontalAlign.LEFT

    VerticalAlign verticalAlign = VerticalAlign.BOTTOM

    String format = "#"

    String prefixText = "Scale: "

    /**
     * Create a scale text from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    ScaleTextItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the Map to use when calculating the map scale
     * @param map The Map
     * @return The ScaleTextItem
     */
    ScaleTextItem map(Map map) {
        this.map = map
        this
    }

    /**
     * Set the number format
     * @param format The number format
     * @return The ScaleTextItem
     */
    ScaleTextItem format(String format) {
        this.format = format
        this
    }

    /**
     * Set the prefix text
     * @param prefixText The prefix text
     * @return The ScaleTextItem
     */
    ScaleTextItem prefixText(String prefixText) {
        this.prefixText = prefixText
        this
    }

    /**
     * Set the text color
     * @param color The text color
     * @return The ScaleTextItem
     */
    ScaleTextItem color(Color color) {
        this.color = color
        this
    }

    /**
     * Set the font
     * @param font The Font
     * @return The ScaleTextItem
     */
    ScaleTextItem font(Font font) {
        this.font = font
        this
    }

    /**
     * Set the horizontal alignment
     * @param horizontalAlign The HorizontalAlign
     * @return The ScaleTextItem
     */
    ScaleTextItem horizontalAlign(HorizontalAlign horizontalAlign) {
        this.horizontalAlign = horizontalAlign
        this
    }

    /**
     * Set the vertical alignment
     * @param verticalAlign The VerticalAlign
     * @return The ScaleTextItem
     */
    ScaleTextItem verticalAlign(VerticalAlign verticalAlign) {
        this.verticalAlign = verticalAlign
        this
    }

    @Override
    String toString() {
        "ScaleTextItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, prefixText = ${prefixText}, color = ${color}, font = ${font}, horizontal-align = ${horizontalAlign}, vertical-align = ${verticalAlign}, map = ${map}, format = ${format})"
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/RectangleItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Color

/**
 * Add a rectangle to a cartographic document.
 * @author <NAME>
 */
class RectangleItem extends Item {

    Color strokeColor = Color.BLACK

    Color fillColor

    float strokeWidth = 1

    /**
     * Create a rectangle from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    RectangleItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the stroke color
     * @param color The stroke color
     * @return The RectangleItem
     */
    RectangleItem strokeColor(Color color) {
        this.strokeColor = color
        this
    }

    /**
     * Set the fill color
     * @param color The fill color
     * @return The RectangleItem
     */
    RectangleItem fillColor(Color color) {
        this.fillColor = color
        this
    }

    /**
     * Set the stroke width
     * @param strokeWidth The stroke width
     * @return The RectangleItem
     */
    RectangleItem strokeWidth(float strokeWidth) {
        this.strokeWidth = strokeWidth
        this
    }

    @Override
    String toString() {
        "RectangleItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, stroke-color = ${strokeColor}, fill-color = ${fillColor}, stroke-width = ${strokeWidth})"
    }

}


<|start_filename|>src/main/groovy/geoscript/layer/io/GmlReader.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.feature.Schema
import geoscript.layer.Cursor
import geoscript.layer.Layer
import geoscript.proj.Projection
import geoscript.workspace.Workspace
import geoscript.workspace.Memory
import net.opengis.wfs.FeatureCollectionType
import org.geotools.feature.FeatureCollection
import org.geotools.xsd.Parser
import org.geotools.wfs.v1_0.WFSConfiguration_1_0 as WFS10
import org.geotools.wfs.v1_1.WFSConfiguration as WFS11
import org.geotools.wfs.v2_0.WFSConfiguration as WFS20

/**
 * Read a {@link geoscript.layer.Layer Layer} from a GML InputStream, File, or String.
 * <p><blockquote><pre>
 * String gml = """<wfs:FeatureCollection>...</wfs:FeatureCollection>"""
 * GmlReader reader = new GmlReader()
 * Layer layer = reader.read(gml)
 * </pre></blockquote></p>
 * @author <NAME>
 */
class GmlReader implements Reader {

    /**
     * Read a GeoScript Layer from an InputStream
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to null)</li>
     *     <li>name: The name of the Layer (defaults to gml)</li>
     *     <li>version: The GML/WFS version (2, 3, or 3.2, defaults to 2)</li>
     * </ul>
     * @param input An InputStream
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], InputStream input) {
        read(options, input, options.get("version", 2) as double)
    }

    /**
     * Read a GeoScript Layer from an InputStream
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to null)</li>
     *     <li>name: The name of the Layer (defaults to gml)</li>
     * </ul>
     * @param input An InputStream
     * @param version The GML/WFS version (2, 3, or 3.2)
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], InputStream input, double version) {
        // Default parameters
        Workspace workspace = options.get("workspace", new Memory())
        Projection proj = options.get("projection")
        String name = options.get("name", "gml")
        // Parse the GML
        def parser = new Parser(getGmlConfig(version))
        def fct = parser.parse(input) as FeatureCollectionType
        def fc = fct.getFeature().get(0) as FeatureCollection
        /*fc.feature.each{f -> ws.ds.addFeatures(f)}
        List<Layer> layers = ws.layers
        if (layers.size() > 1) {
            return layers
        } else {
            return layers[0]
        }*/
        // Create Schema and Layer
        Schema schema = new Schema(fc.schema).reproject(proj, name)
        Layer layer = workspace.create(schema)
        layer.add(new Cursor(fc))
        layer


    }

    /**
     * Read a GeoScript Layer from a File
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to null)</li>
     *     <li>name: The name of the Layer (defaults to gml)</li>
     *     <li>version: The GML/WFS version (2, 3, or 3.2, defaults to 2)</li>
     * </ul>
     * @param file A File
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], File file) {
        read(options, file, options.get("version", 2) as double)
    }

    /**
     * Read a GeoScript Layer from a File
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to null)</li>
     *     <li>name: The name of the Layer (defaults to gml)</li>
     * </ul>
     * @param file A File
     * @param version The GML/WFS version (2, 3, or 3.2)
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], File file, double version) {
        read(options, new FileInputStream(file), version)
    }

    /**
     * Read a GeoScript Layer from a String
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to null)</li>
     *     <li>name: The name of the Layer (defaults to gml)</li>
     *     <li>version: The GML/WFS version (2, 3, or 3.2, defaults to 2)</li>
     * </ul>
     * @param str A String
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], String str) {
        read(options, str, options.get("version", 2) as double)
    }

    /**
     * Read a GeoScript Layer from a String
     * @param options The optional named parameters:
     * <ul>
     *     <li>workspace: The Workspace used to create the Layer (defaults to Memory)</li>
     *     <li>projection: The Projection assigned to the Layer (defaults to null)</li>
     *     <li>name: The name of the Layer (defaults to gml)</li>
     * </ul>
     * @param str A String
     * @param version The GML/WFS version (2, 3, or 3.2)
     * @return A GeoScript Layer
     */
    Layer read(Map options = [:], String str, double version) {
        read(options, new ByteArrayInputStream(str.getBytes("UTF-8")), version)
    }

    /**
     * Get the correct GML Configuration based on the version
     * @param version The version number (2, 3, or 3.2)
     * @return  A GeoTools GML Configuration
     */
    private def getGmlConfig(double version) {
        if (version == 2) {
            return new WFS10()
        } else if (version == 3) {
            return new WFS11()
        } else /*if (version == 3.2)*/ {
            return new WFS20()
        }
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/MapItem.groovy<|end_filename|>
package geoscript.carto

import geoscript.render.Map

/**
 * Add a Map to a cartographic document.
 * @author <NAME>
 */
class MapItem extends Item {

    Map map

    /**
     * Create a Map from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    MapItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the Map
     * @param map The Map
     * @return The MapItem
     */
    MapItem map(Map map) {
        this.map = map
        this
    }

    @Override
    String toString() {
        "MapItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, map = ${map})"
    }

}


<|start_filename|>src/test/groovy/geoscript/plot/ChartTest.groovy<|end_filename|>
package geoscript.plot

import org.junit.jupiter.api.io.TempDir

import javax.imageio.ImageIO

import static org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

/**
 * The Chart Unit Test
 * @author <NAME>
 */
class ChartTest {

    @TempDir
    File folder

    @Test void constructor() {
        List data = [
            [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart = Bar.xy(data)
        assertNotNull chart
        assertNotNull chart.chart
    }

    @Test void save() {
        List data = [
            [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart = Bar.xy(data)
        File file = new File(folder,"xy.png")
        chart.save(file)
        assertTrue file.exists()
        assertTrue file.length() > 0

        file = new File(folder,"xy.jpg")
        chart.save(file)
        assertTrue file.exists()
        assertTrue file.length() > 0

        file = new File(folder,"xy.jpeg")
        chart.save(file)
        assertTrue file.exists()
        assertTrue file.length() > 0
    }

    @Test void saveGif() {
        assertThrows(IllegalArgumentException) {
            List data = [
                    [1, 10], [45, 12], [23, 3], [5, 20]
            ]
            Chart chart = Bar.xy(data)
            File file = new File(folder,"xy.gif")
            chart.save(file)
        }
    }

    @Test void getImage() {
        List data = [
            [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart = Bar.xy(data)
        def image = chart.image
        assertNotNull image
        File file = new File(folder,"xy.png")
        ImageIO.write(image, "png", file)
        assertTrue file.exists()
        assertTrue file.length() > 0
    }

    @Test void overlay() {
        List data = [
            [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart1 = Bar.xy(data)
        Chart chart2 = Curve.curve(data)
        Chart chart3 = Regression.linear(data)
        chart1.overlay([chart2,chart3])
        File file = new File(folder,"overlay.png")
        chart1.save(file)
    }
}


<|start_filename|>src/test/groovy/geoscript/style/FillTest.groovy<|end_filename|>
package geoscript.style

import geoscript.layer.Shapefile
import geoscript.render.Draw
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.filter.Expression
import geoscript.filter.Color
import geoscript.filter.Function

/**
 * The Fill Unit Test
 * @author <NAME>
 */
class FillTest {

    @TempDir
    File folder

    @Test void constructors() {

        // Create a simple Fill
        Fill fill = new Fill("#ffffff", 0.75)
        assertEquals "#ffffff", fill.color.value
        assertEquals 0.75, fill.opacity.value, 0.01
        assertEquals "Fill(color = #ffffff, opacity = 0.75)", fill.toString()

        def gtFill = fill.createFill()
        assertEquals "#ffffff", gtFill.color.value
        assertEquals 0.75, gtFill.opacity.value, 0.01

        // Create a simple Fill with Expressions
        fill = new Fill(new Color("#ffffff"), new Expression(0.75))
        assertEquals "#ffffff", fill.color.value
        assertEquals 0.75, fill.opacity.value, 0.01
        assertEquals "Fill(color = #ffffff, opacity = 0.75)", fill.toString()

        gtFill = fill.createFill()
        assertEquals "#ffffff", gtFill.color.value
        assertEquals 0.75, gtFill.opacity.value, 0.01

        // Add Hatch
        fill.hatch("slash", new Stroke("wheat"), 4)
        assertEquals "shape://slash", fill.hatch.name.value
        assertEquals "#f5deb3", fill.hatch.stroke.color.value
        assertEquals 1.0, fill.hatch.stroke.width.value, 0.1
        assertEquals 4.0, fill.hatch.size.value, 0.1

        gtFill = fill.createFill();
        assertEquals "#ffffff", gtFill.color.value
        assertEquals 0.75, gtFill.opacity.value, 0.01
        assertEquals "shape://slash", gtFill.graphicFill.graphicalSymbols()[0].wellKnownName.value
        assertEquals "#f5deb3", gtFill.graphicFill.graphicalSymbols()[0].stroke.color.value
        assertEquals 1.0, gtFill.graphicFill.graphicalSymbols()[0].stroke.width.value, 0.1
        assertEquals 4.0, gtFill.graphicFill.size.value, 0.1

        // Add Icon
        fill.icon("http://www.geotools.org/_static/img/geotools-logo.png", "image/png")
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), fill.icon.url
        assertTrue fill.icon.url instanceof URL
        assertEquals "image/png", fill.icon.format

        // Named parameters
        fill = new Fill(color: "red", opacity:0.22)
        assertEquals "#ff0000", fill.color.value
        assertEquals 0.22, fill.opacity.value, 0.1

        // Random fill
        fill = new Fill(null).hatch("slash", new Stroke("#000088",4,null,"round"), 8).random([
                random:"free", symbolCount: "36", seed: "5", tileSize: "100", rotation:"none"
        ])
        assertEquals fill.options["random"], "free"
        assertEquals fill.options["random-seed"], "5"
        assertEquals fill.options["random-symbol-count"], "36"
        assertEquals fill.options["random-tile-size"], "100"
        assertEquals fill.options["random-rotation"], "none"
    }

    @Test void apply() {
        def sym = Symbolizer.styleFactory.createPolygonSymbolizer()
        Fill fill = new Fill("#ffffff", 0.75).icon("http://www.geotools.org/_static/img/geotools-logo.png", "image/png")
        fill.apply(sym)
        assertEquals "#ffffff", sym.fill.color.value
        assertEquals 0.75, sym.fill.opacity.value, 0.01
        assertNotNull(sym.fill.graphicFill)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), sym.fill.graphicFill.graphicalSymbols()[0].location
        assertEquals "image/png", sym.fill.graphicFill.graphicalSymbols()[0].format
    }

    @Test void prepare() {
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createPolygonSymbolizer())
        Fill fill = new Fill("#ffffff", 0.75).icon("http://www.geotools.org/_static/img/geotools-logo.png", "image/png")
        fill.prepare(rule)
        def sym = rule.symbolizers[0]
        assertEquals "#ffffff", sym.fill.color.value
        assertEquals 0.75, sym.fill.opacity.value, 0.01
        assertNotNull(sym.fill.graphicFill)
        assertEquals new URL("http://www.geotools.org/_static/img/geotools-logo.png"), sym.fill.graphicFill.graphicalSymbols()[0].location
        assertEquals "image/png", sym.fill.graphicFill.graphicalSymbols()[0].format
    }

    @Test void recode() {
        Fill fill = new Fill(new Function("Recode(SUB_REGION,'N Eng','#6495ED')"))
        assertNotNull fill
        assertTrue(fill.color instanceof Function)
    }

    @Test void randomizedFill() {
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shp = new Shapefile(shpFile)
        File file = new File(folder,"randomized_fill.png")
        shp.style = (new Fill(null).hatch("circle", new Fill("#aaaaaa"), 1).random([random:"free", symbolCount: "50", tileSize: "100"]).where("PERSONS < 2000000")) +
                (new Fill(null).hatch("circle", new Fill("#aaaaaa"), 2).random([random:"free", symbolCount: "200", tileSize: "100"]).where("PERSONS BETWEEN 2000000 AND 4000000")) +
                (new Fill(null).hatch("circle", new Fill("#aaaaaa"), 2).random([random:"free", symbolCount: "700", tileSize: "100"]).where("PERSONS > 4000000")) +
                (new Stroke("black",0.1) + new Label(property: "STATE_ABBR", font: new Font(family: "Times New Roman", style: "normal", size: 14)).point([0.5,0.5]).halo(new Fill("#FFFFFF"),2))
        Draw.draw(shp, out: file, backgroundColor: "white")
        assertTrue file.exists()
        assertTrue file.size() > 0
    }

}


<|start_filename|>src/test/groovy/geoscript/workspace/WorkspaceFactoriesTest.groovy<|end_filename|>
package geoscript.workspace

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertTrue

/**
 * The WorkspaceFactories Unit Test
 * @author <NAME>
 */
class WorkspaceFactoriesTest {

    @Test void list() {
        List<WorkspaceFactory> factories = WorkspaceFactories.list()
        assertNotNull factories
        assertTrue factories.size() > 0
    }

}


<|start_filename|>src/test/groovy/geoscript/style/ShadedReliefTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The ShadedRelief UnitTest
 * @author <NAME>
 */
class ShadedReliefTest {

    @Test void constructors() {
        def shadedRelief = new ShadedRelief()
        assertEquals 55, shadedRelief.reliefFactor.value
        assertFalse shadedRelief.brightnessOnly

        shadedRelief = new ShadedRelief(35, true)
        assertEquals 35, shadedRelief.reliefFactor.value
        assertTrue shadedRelief.brightnessOnly
    }

    @Test void apply() {
        def shadedRelief = new ShadedRelief()
        def sym = Symbolizer.styleFactory.createRasterSymbolizer()
        shadedRelief.apply(sym)
        assertEquals 55, sym.shadedRelief.reliefFactor.value
        assertFalse sym.shadedRelief.brightnessOnly

        shadedRelief = new ShadedRelief(35, true)
        sym = Symbolizer.styleFactory.createRasterSymbolizer()
        shadedRelief.apply(sym)
        assertEquals 35, sym.shadedRelief.reliefFactor.value
        assertTrue sym.shadedRelief.brightnessOnly
    }

    @Test void prepare() {
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        def shadedRelief = new ShadedRelief()
        shadedRelief.prepare(rule)
        def sym = rule.symbolizers[0]
        assertEquals 55, sym.shadedRelief.reliefFactor.value
        assertFalse sym.shadedRelief.brightnessOnly

        rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        shadedRelief = new ShadedRelief(35, true)
        sym = Symbolizer.styleFactory.createRasterSymbolizer()
        shadedRelief.prepare(rule)
        sym = rule.symbolizers[0]
        assertEquals 35, sym.shadedRelief.reliefFactor.value
        assertTrue sym.shadedRelief.brightnessOnly
    }

    @Test void string() {
        def shadedRelief = new ShadedRelief()
        assertEquals "ShadedRelief(reliefFactor = 55, brightnessOnly = false)", shadedRelief.toString()

        shadedRelief = new ShadedRelief(35, true)
        assertEquals "ShadedRelief(reliefFactor = 35, brightnessOnly = true)", shadedRelief.toString()
    }
}


<|start_filename|>src/test/groovy/geoscript/style/io/YSLDWriterTest.groovy<|end_filename|>
package geoscript.style.io

import geoscript.AssertUtil
import geoscript.style.Fill
import geoscript.style.Stroke
import geoscript.style.Symbolizer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The SLDWriter UnitTest
 * @author <NAME>
 */
class YSLDWriterTest {

    @TempDir
    File folder

    private String expectedYaml = """name: Default Styler
feature-styles:
- name: name
  rules:
  - scale: [min, max]
    symbolizers:
    - polygon:
        fill-color: '#F5DEB3'
    - line:
        stroke-color: '#A52A2A'
        stroke-width: 1
"""

    @Test void writeToOutputStream() {
        Symbolizer sym = new Fill("wheat") + new Stroke("brown")
        YSLDWriter writer = new YSLDWriter()
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(sym, out)
        String yaml = out.toString().trim()
        assertNotNull yaml
        assertTrue yaml.length() > 0
        AssertUtil.assertStringsEqual expectedYaml, yaml, trim: true
    }

    @Test void writeToFile() {
        Symbolizer sym = new Fill("wheat") + new Stroke("brown")
        YSLDWriter writer = new YSLDWriter()
        File file = new File(folder,"simple.yaml")
        writer.write(sym, file)
        String yaml = file.text.trim()
        assertNotNull yaml
        assertTrue yaml.length() > 0
        AssertUtil.assertStringsEqual expectedYaml, yaml, trim: true
    }

    @Test void writeToString() {
        Symbolizer sym = new Fill("wheat") + new Stroke("brown")
        YSLDWriter writer = new YSLDWriter()
        String yaml = writer.write(sym).trim()
        assertNotNull yaml
        assertTrue yaml.length() > 0
        AssertUtil.assertStringsEqual expectedYaml, yaml, trim: true
    }

    @Test void writer() {
        Writer writer = Writers.find("ysld")
        assertNotNull writer
        assertTrue writer instanceof YSLDWriter
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/io/WritersTest.groovy<|end_filename|>
package geoscript.layer.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The Layer Writers Unit Test.
 * @author <NAME>
 */
class WritersTest {

    @Test void list() {
        List<Writer> writers = Writers.list()
        assertNotNull writers
        assertTrue writers.size() > 0
    }

    @Test void find() {
        Writer writer = Writers.find("csv")
        assertNotNull writer

        writer = Writers.find("asdf")
        assertNull writer
    }
}


<|start_filename|>src/test/groovy/geoscript/carto/PdfCartoFactoryTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue

class PdfCartoFactoryTest {

    @Test
    void getName() {
        assertEquals("pdf", new PdfCartoFactory().getName())
    }

    @Test
    void getMimeType() {
        assertEquals("application/pdf", new PdfCartoFactory().getMimeType())
    }

    @Test
    void create() {
        PdfCartoFactory cartoFactory = new PdfCartoFactory()
        CartoBuilder cartoBuilder = cartoFactory.create(PageSize.LETTER_LANDSCAPE)
        assertTrue(cartoBuilder instanceof PdfCartoBuilder)
    }
}


<|start_filename|>src/test/groovy/geoscript/carto/io/JsonCartoReaderTest.groovy<|end_filename|>
package geoscript.carto.io

import geoscript.carto.CartoBuilder
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

class JsonCartoReaderTest {

    @Test void getName() {
        CartoReader cartoReader = new JsonCartoReader()
        assertEquals("json", cartoReader.name)
    }

    @Test void readCarto() {
        String json = """{
          "type": "png",
          "width": 792, 
          "height": 612,
          "items": [
            {
                "x": 0,
                "y": 0,
                "width": 792,
                "height": 612,
                "type": "rectangle",
                "fillColor": "white",
                "strokeColor": "white"
            },
            {
                "x": 0,
                "y": 0,
                "width": 792,
                "height": 612,
                "type": "grid",
                "strokeWidth": 1,
                "strokeColor": "gray",
                "size": 10,
            },
            {
                "x": 10,
                "y": 10,
                "width": 772,
                "height": 592,
                "type": "rectangle",
                "strokeWidth": 1.0,
                "strokeColor": "black"
            },
            {
                "x": 20,
                "y": 20,
                "width": 752,
                "height": 80,
                "type": "rectangle"
            },
            {
                "x": 30,
                "y": 50,
                "width": 200,
                "height": 20,
                "type": "text",
                "text": "Map Title",
                "color": "Black",
                "horizontalAlign": "left",
                "verticalAlign": "bottom",
                "font": {
                    "name": "Arial",
                    "style": "Bold",
                    "size": 36
                }
            },
            {
                "x": 30,
                "y": 85,
                "width": 200,
                "height": 10,
                "type": "dateText",
                "color": "black",
                "format": "MM/dd/yyyy",
                "date": "12/29/2020",
                "horizontalAlign": "left",
                "verticalAlign": "bottom",
                "font": {
                    "name": "Arial",
                    "style": "Italic",
                    "size": 18
                }
            },
            {
                "x": 250,
                "y": 30,
                "width": 380,
                "height": 70,
                "type": "paragraph",
                "text": "Permission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the Software), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.",
                "color": "black",
                "font": {
                    "name": "Arial",
                    "style": "Plain",
                    "size": 8
                }
            },
            {
                "x": 710,
                "y": 30,
                "width": 1,
                "height": 60,
                "type": "line",
                "strokeColor": "black",
                "strokeWidth": 1
            },
            {
                "x": 640,
                "y": 30,
                "width": 60,
                "height": 60,
                "type": "image",
                "path": "${new File(getClass().getClassLoader().getResource("image.png").toURI()).absolutePath}"
            },
            {
                "x": 720,
                "y": 30,
                "width": 40,
                "height": 60,
                "type": "northArrow",
                "style": "North",
                "fillColor1": "black",
                "strokeColor1": "black",
                "fillColor2": "white",
                "strokeColor2": "black",
                "strokeWidth": 1,
                "textColor": "black",
                "drawText": false,
                "font": {
                    "name": "Arial",
                    "style": "Bold",
                    "size": 48
                }
            },
            {
                "x": 20,
                "y": 110,
                "width": 752,
                "height": 480,
                "type": "rectangle"
            },
            {
                "x": 30,
                "y": 120,
                "width": 742,
                "height": 470,
                "type": "map",
                "name": "mainMap",
                "layers": [
                    {"layertype": "layer", "file": "${new File("src/test/resources/states.shp").absolutePath}"}
                ]
            },
            {
                "x": 30,
                "y": 490,
                "width": 100,
                "height": 90,
                "type": "overViewMap",
                "linkedMap": "mainMap",
                "layers": [
                    {"layertype": "raster", "source": "${new File("src/test/resources/raster.tif").absolutePath}"}
                ]
            },
            {
                "x": 150,
                "y": 490,
                "width": 100,
                "height": 90,
                "type": "overViewMap",
                "linkedMap": "mainMap",
                "zoomIntoBounds": true,
                "scaleFactor": 3.0,
                "layers": [
                    {"layertype": "raster", "source": "${new File("src/test/resources/raster.tif").absolutePath}"}
                ]
            },
            {
                "x": 460,
                "y": 120,
                "width": 300,
                "height": 200,
                "type": "table",
                "columns": ["ID","Name"],
                "rows": [
                    {"ID": 1, "Name": "One"},
                    {"ID": 2, "Name": "Two"},
                    {"ID": 3, "Name": "Three"}
                ]
            },
            {
                "x": 640,
                "y": 500,
                "width": 120,
                "height": 80,
                "type": "legend",
                "map": "mainMap",
                "backgroundColor": "white",
                "title": "Legend",
                "titleFont": {
                    "name": "Arial",
                    "style": "bold",
                    "size": 18
                },
                "titleColor": "black",
                "textFont": {
                    "name": "Arial",
                    "style": "plain",
                    "size": 12
                },
                "textColor": "black",
                "legendEntryWidth": 50,
                "legendEntryHeight": 30,
                "gapBetweenEntries": 10,
                "numberFormat": "#.##"
            },
            {
                "x": 150,
                "y": 85,
                "width": 200,
                "height": 10,
                "type": "scaleText",
                "map": "mainMap",
                "color": "black",
                "horizontalAlign": "left",
                "verticalAlign": "bottom",
                "format": "#",
                "prefixText": "Scale: ",
                "font": {
                    "name": "Arial",
                    "style": "italic",
                    "size": 18
                }
            },
            {
                "x": 50,
                "y": 125,
                "width": 200,
                "height": 20,
                "type": "scaleBar",
                "map": "mainMap",
                "strokeColor": "black",
                "fillColor": "white",
                "strokeWidth": 1,
                "font": {
                    "name": "Arial",
                    "style": "plain",
                    "size": 12
                },
                "units": "METRIC",
                "border": 5
            }
          ] 
        }"""
        CartoReader cartoReader = new JsonCartoReader()
        CartoBuilder cartoBuilder = cartoReader.read(json)
        boolean saveToTarget = false
        File file = saveToTarget ? new File("target/carto_json.png"): File.createTempFile("carto_json","png")
        file.withOutputStream { OutputStream outputStream ->
            cartoBuilder.build(outputStream)
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/io/MvtWriterTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.layer.Layer
import geoscript.layer.Shapefile
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The MvtWriter Unit Test
 * @author <NAME>
 */
class MvtWriterTest {

    @TempDir
    File folder

    @Test void writeToFile() {
        URL url = getClass().getClassLoader().getResource("states.shp")
        Layer layer = new Shapefile(new File(url.toURI()))
        File file = new File(folder,"states.mvt")
        Writer writer = new MvtWriter()
        writer.write(layer, file)
        assertTrue file.length() > 0
    }

    @Test void writeToOutputStream() {
        URL url = getClass().getClassLoader().getResource("states.shp")
        Layer layer = new Shapefile(new File(url.toURI()))
        File file = new File(folder,"states.mvt")
        OutputStream out = new FileOutputStream(file)
        Writer writer = new MvtWriter()
        writer.write(layer, out)
        out.close()
        assertTrue file.length() > 0
    }

    @Test void writeToString() {
        URL url = getClass().getClassLoader().getResource("states.shp")
        Layer layer = new Shapefile(new File(url.toURI()))
        Writer writer = new MvtWriter()
        String str = writer.write(layer)
        assertTrue str.length() > 0
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/io/TWkbReaderTest.groovy<|end_filename|>
package geoscript.geom.io

import geoscript.geom.Geometry
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

class TWkbReaderTest {

    @Test
    void read() {
        TWkbReader reader = new TWkbReader()
        Geometry geometry = reader.read("01000204")
        assertEquals("POINT (1 2)", geometry.wkt)
        geometry = reader.read("01000204".decodeHex())
        assertEquals("POINT (1 2)", geometry.wkt)
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/ParagraphItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.*

class ParagraphItemTest {

    @Test
    void create() {

        ParagraphItem item = new ParagraphItem(10,20,300,400)
            .font(new Font("Verdana", Font.BOLD, 14))
            .color(Color.BLUE)
            .text("Text Here")

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(item.font.name, "Verdana")
        assertEquals(item.font.style, Font.BOLD)
        assertEquals(item.font.size, 14)
        assertEquals(item.color, Color.BLUE)
        assertEquals("Text Here", item.text)
        assertTrue(item.toString().startsWith("ParagraphItem(x = 10, y = 20, width = 300, height = 400, " +
                "text = Text Here, color = java.awt.Color[r=0,g=0,b=255], font = "))
        assertTrue(item.toString().endsWith("style=bold,size=14])"))
    }


}


<|start_filename|>src/test/groovy/geoscript/FileUtil.groovy<|end_filename|>
package geoscript

class FileUtil {

    static File createDir(File dir, String name) {
        File file = new File(dir, name)
        file.mkdir()
        file
    }

}


<|start_filename|>src/main/groovy/geoscript/geom/io/GeoRSSWriter.groovy<|end_filename|>
/*
 *  The MIT License
 * 
 *  Copyright 2010 <NAME>.
 * 
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 * 
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 * 
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 */

package geoscript.geom.io

import org.locationtech.jts.geom.Coordinate
import geoscript.geom.Geometry
import geoscript.geom.LineString
import geoscript.geom.Point
import geoscript.geom.Polygon

/**
 * A GeoRSS Simple Writer.  See the <a href="http://www.georss.org/simple">GeoRSS spec</a> for more details.
 * Only Points, LineStrings, and Polygon are supported.  Any other {@link geoscript.geom.Geometry Geometry} type
 * will return a null value.
 * <p><blockquote><pre>
 * GeoRSSWriter writer = new GeoRSSWriter()
 * String georss = writer.write(new {@link geoscript.geom.Point Point}(-71.92, 45.256))
 *
 * &lt;georss:point&gt;45.256 -71.92&lt;/georss:point&gt;
 * </pre></blockquote></p>
 * @author <NAME>
 */
class GeoRSSWriter implements Writer {

    /**
     * The type of encoding (simple, gml, w3c)
     */
    String type = "simple"

    /**
     * Write a Geometry to a String
     * @param g The Geometry
     * @return A String
     */
    String write(Geometry geom) {
        if (type.equalsIgnoreCase("simple")) {
            writeSimple(geom)
        } else if (type.equalsIgnoreCase("gml")) {
            writeGml(geom)
        } else if (type.equalsIgnoreCase("w3c")) {
            writeW3c(geom)
        } else {
            null
        }
    }

    /**
     * Write the Geometry to a GeoRSS Simple XML String
     * See http://georss.org/simple.html for more details.
     * @param geom The Geometry
     * @return A GeoRSS Simple XML String
     */
    private String writeSimple(Geometry geom) {
        if (geom instanceof Point) {
            return "<georss:point>${geom.y} ${geom.x}</georss:point>"
        } else if (geom instanceof LineString) {
            return "<georss:line>${getCoordinatesAsString(geom.coordinates)}</georss:line>"
        } else if (geom instanceof Polygon) {
            return "<georss:polygon>${getCoordinatesAsString(geom.exteriorRing.coordinates)}</georss:polygon>"
        }
        return null
    }

    /**
     * Write the Geometry to a GeoRSS GML XML String
     * See http://georss.org/gml.html for more details.
     * @param geom The Geometry
     * @return A GeoRSS GML XML String
     */
    private String writeGml(Geometry geom) {
        if (geom instanceof Point) {
            return "<georss:where><gml:Point><gml:pos>${geom.y} ${geom.x}</gml:pos></gml:Point></georss:where>"
        } else if (geom instanceof LineString) {
            return "<georss:where><gml:LineString><gml:posList>${getCoordinatesAsString(geom.coordinates)}</gml:posList></gml:LineString></georss:where>"
        } else if (geom instanceof Polygon) {
            return "<georss:where><gml:Polygon><gml:exterior><gml:LinearRing><gml:posList>${getCoordinatesAsString(geom.exteriorRing.coordinates)}</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></georss:where>"
        }
        return null
    }

    /**
     * Write the Geometry to a W3C encoded GeoRSS Xml String.
     * See http://georss.org/w3c.html for more details.
     * @param geom The Geometry.
     * @return A String or null
     */
    private String writeW3c(Geometry geom) {
        if (geom instanceof Point) {
            return "<geo:Point><geo:lat>${geom.y}</geo:lat><geo:long>${geom.x}</geo:long></geo:Point>"
        }
        return null
    }

    /**
     * Write an Array of Coordinates to a GML String
     * @param coords The Array of Coordinates
     * @return A GML String (x1,y1 x2,y2)
     */
    private String getCoordinatesAsString(Coordinate[] coords) {
        coords.collect { c -> "${c.y} ${c.x}" }.join(" ")
    }

    /**
     * Build a GeoRSS Geometry using a Groovy MarkupBuilder
     * @param options The named parameters
     * <ul>
     *      <li>projId = The Projection ID (defaults to null)</li>
     * </ul>
     * @param builder The MarkupBuilder Node
     * @param geom The Geometry
     */
    void write(Map options = [:], def builder, Geometry geom) {
        String projId = options.get("projId")
        if (type.equalsIgnoreCase("simple")) {
            if (geom instanceof Point) {
                Point pt = geom as Point
                builder.georss.point "${pt.y} ${pt.x}"
            } else if (geom instanceof LineString) {
                LineString line = geom as LineString
                builder.georss.line "${getCoordinatesAsString(line.coordinates)}"
            } else if (geom instanceof Polygon) {
                Polygon poly = geom as Polygon
                builder.georss.polygon "${getCoordinatesAsString(poly.exteriorRing.coordinates)}"
            }
        } else if (type.equalsIgnoreCase("gml")) {
            if (geom instanceof Point) {
                Point pt = geom as Point
                builder.georss.where {
                    gml.Point(projId && !projId.equals("4326") ? [srsName: "urn:ogc:def:crs:${projId}"] : [:]) {
                        gml.pos "${pt.y} ${pt.x}"
                    }
                }
            } else if (geom instanceof LineString) {
                LineString line = geom as LineString
                builder.georss.where {
                    gml.LineString(projId && !projId.equals("4326") ? [srsName: "urn:ogc:def:crs:${projId}"] : [:]) {
                        gml.posList "${getCoordinatesAsString(line.coordinates)}"
                    }
                }
            } else if (geom instanceof Polygon) {
                Polygon poly = geom as Polygon
                builder.georss.where {
                    gml.Polygon(projId && !projId.equals("4326") ? [srsName: "urn:ogc:def:crs:${projId}"] : [:]) {
                        gml.LinearRing {
                            gml.posList "${getCoordinatesAsString(poly.exteriorRing.coordinates)}"
                        }
                    }
                }
            }
        } else if (type.equalsIgnoreCase("w3c")) {
            if (geom instanceof Point) {
                Point pt = geom as Point
                builder.geo.Point {
                    geo.lat pt.y
                    geo.long pt.x
                }
            }
        }
    }
}



<|start_filename|>src/test/groovy/geoscript/style/RasterSymbolizerTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The Raster UnitTest
 * @author <NAME>
 */
class RasterSymbolizerTest {

    @Test void raster() {
        RasterSymbolizer raster = new RasterSymbolizer(0.5)
        assertEquals 0.5, raster.opacity.value, 0.1
        assertEquals "RANDOM", raster.overlap.value
        assertEquals "grid", raster.geometry.value

        raster = new RasterSymbolizer(0.75, "AVERAGE", "the_geom")
        assertEquals 0.75, raster.opacity.value, 0.1
        assertEquals "AVERAGE", raster.overlap.value
        assertEquals "the_geom", raster.geometry.value
    }

    @Test void apply() {
        RasterSymbolizer raster = new RasterSymbolizer(0.5)
        def sym = Symbolizer.styleFactory.createRasterSymbolizer()
        raster.apply(sym)
        assertEquals 0.5, sym.opacity.value
        assertEquals "RANDOM", sym.overlap.value.toString()
        assertEquals "grid", sym.geometry.value

        raster = new RasterSymbolizer(0.75, "AVERAGE", "the_geom")
        sym = Symbolizer.styleFactory.createRasterSymbolizer()
        raster.apply(sym)
        assertEquals 0.75, sym.opacity.value
        assertEquals "AVERAGE", sym.overlap.value
        assertEquals "the_geom", sym.geometry.value
    }

    @Test void prepare() {
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        RasterSymbolizer raster = new RasterSymbolizer(0.5)
        raster.prepare(rule)
        def sym = rule.symbolizers[0]
        assertEquals 0.5, sym.opacity.value
        assertEquals "RANDOM", sym.overlap.value.toString()
        assertEquals "grid", sym.geometry.value
    }

    @Test void string() {
        RasterSymbolizer raster = new RasterSymbolizer(0.5)
        assertEquals "Raster(opacity = 0.5, overlap = RANDOM, geometry = grid)", raster.toString()
        raster = new RasterSymbolizer(0.75, "AVERAGE", "the_geom")
        assertEquals "Raster(opacity = 0.75, overlap = AVERAGE, geometry = the_geom)", raster.toString()
    }
}


<|start_filename|>src/test/groovy/geoscript/carto/LegendItemTest.groovy<|end_filename|>
package geoscript.carto

import geoscript.layer.Shapefile
import geoscript.style.ColorMap
import geoscript.style.Fill
import geoscript.style.Shape
import geoscript.style.Stroke
import java.awt.Color
import org.junit.jupiter.api.Test

import java.awt.Font

import static org.junit.jupiter.api.Assertions.*

class LegendItemTest {

    @Test
    void create() {

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Shapefile shapefile = new Shapefile(file)
        shapefile.style = new Fill("white") + new Stroke("black", 0.1)

        LegendItem item = new LegendItem(10,20,300,400)
            .title("Legend")
            .titleFont(new Font("Verdana", Font.BOLD, 24))
            .titleColor(Color.BLACK)
            .backgroundColor(Color.WHITE)
            .textFont(new Font("Verdana", Font.PLAIN, 18))
            .textColor(Color.BLUE)
            .legendEntryWidth(25)
            .legendEntryHeight(25)
            .gapBetweenEntries(15)
            .numberFormat("#")
            .addPointEntry("Cities", new Shape("Black", 8, "circle"))
            .addLineEntry("Rivers", new Stroke("Blue", 2))
            .addPolygonEntry("Parcels", new Fill("red", 0.75) + new Stroke("black",1))
            .addGroupEntry("Raster")
            .addColorMapEntry("Potential", new ColorMap(0, 2300, geoscript.filter.Color.getPaletteColors("YellowToRedHeatMap", 7)))
            .addLayer(shapefile)

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertTrue(item.toString().startsWith("LegendItem(x = 10, y = 20, width = 300, height = 400,"))
        assertTrue(item.toString().endsWith(")"))
    }

}


<|start_filename|>src/main/groovy/geoscript/workspace/FlatGeobuf.groovy<|end_filename|>
package geoscript.workspace

import org.geotools.data.DataStore
import org.geotools.data.flatgeobuf.FlatGeobufDataStoreFactory
import org.geotools.data.flatgeobuf.FlatGeobufDirectoryDataStore
import org.geotools.util.URLs

/**
 * A FlatGeobuf Workspace.
 * @author <NAME>
 */
class FlatGeobuf extends Workspace {

    /**
     * The directory of fgb files
     */
    private File directory
    
    /**
     * Create a new FlatGeobuf Workspace
     * @param directory The directory of fgb files
     */
    FlatGeobuf(File directory) {
        this(createDataStore(directory))
        this.directory = directory
    }

    /**
     * Create a new FlatGeobuf Workspace from a FlatGeobufDirectoryDataStore
     * @param ds The FlatGeobufDirectoryDataStore
     */
    FlatGeobuf(FlatGeobufDirectoryDataStore ds) {
        super(ds)
    }

    /**
     * Get the format
     * @return The workspace format name
     */
    @Override
    String getFormat() {
        return "FlatGeobuf"
    }

    /**
     * Remove a Layer by name from this Workspace
     * @param name The Layer name
     */
    @Override
    void remove(String name) {
        File file = new File(directory, name.endsWith(".fgb") ? name : "${name}.fgb")
        if (file.exists()) {
            file.delete()
        }
    }

    private static DataStore createDataStore(File directory) {
        Map params = new java.util.HashMap()
        params.put("url", URLs.fileToUrl(directory))
        FlatGeobufDataStoreFactory factory = new FlatGeobufDataStoreFactory()
        factory.createDataStore(params)
    }

    /**
     * The FlatGeobuf WorkspaceFactory
     */
    static class Factory extends WorkspaceFactory<FlatGeobuf> {

        @Override
        Map getParametersFromString(String str) {
            Map params = [:]
            if (!str.contains("=") && str.endsWith(".fgb")) {
                File file = new File(str).absoluteFile
                if (!file.isDirectory()) {
                    file = file.parentFile
                }
                params.put("type", "flatgeobuf")
                params.put("url", URLs.fileToUrl(file))
            } else {
                params = super.getParametersFromString(str)
            }
            params
        }

        @Override
        FlatGeobuf create(String type) {
            if (!type.contains("=") && type.endsWith(".fgb")) {
                File file = new File(type)
                new FlatGeobuf(file.isDirectory() ? file : file.getParentFile())
            } else {
                null
            }
        }

        @Override
        FlatGeobuf create(String type, Map params) {
            if (type.equalsIgnoreCase('flatgeobuf') && params.containsKey('file')) {
                File file = params.get('file') instanceof File ? params.get('file') : new File(params.get('file'))
                if (!file.isDirectory()) {
                    file = file.parentFile
                }
                new FlatGeobuf(file)
            } else if (params.containsKey('url') && params["url"]?.toString().endsWith(".fgb")) {
                File file = params.get('url') instanceof File ? params.get('url') : new File(params.get('url'))
                if (!file.isDirectory()) {
                    file = file.parentFile
                }
                new FlatGeobuf(file)
            } else {
                null
            }
        }

        @Override
        FlatGeobuf create(DataStore dataStore) {
            if (dataStore != null && dataStore instanceof org.geotools.data.flatgeobuf.FlatGeobufDirectoryDataStore) {
                new FlatGeobuf(dataStore)
            } else {
                null
            }
        }
    }
}


<|start_filename|>src/test/groovy/geoscript/workspace/SpatiaLiteTest.groovy<|end_filename|>
package geoscript.workspace

import geoscript.FileUtil
import geoscript.layer.Shapefile
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.layer.Layer

/**
 * The SpatiaLite Unit Test
 */
class SpatiaLiteTest {

    @TempDir
    File folder

    private boolean shouldRunTest() {
        boolean isAvailable = OGR.isAvailable()
        if (!isAvailable) {
            println "OGR is not available!"
        } else {
            OGR.setErrorHandler("quiet")
        }
        isAvailable
    }

    @Test void writeReadSpatialite() {
        if (shouldRunTest()) {
            File file = new File(FileUtil.createDir(folder,"states_sqlite"), "states.sqlite")
            SpatiaLite spatialite = new SpatiaLite(file)
            File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
            Layer shpLayer = new Shapefile(shpFile)
            Layer layer = spatialite.create(shpLayer.cursor)
            assertNotNull layer
            assertEquals "SpatiaLite", spatialite.format
            assertTrue spatialite.toString().startsWith("SpatiaLite(")
            assertTrue spatialite.toString().endsWith("states.sqlite)")
            assertEquals 1, spatialite.names.size()
            assertTrue spatialite.names.contains("states")
            assertEquals 1, spatialite.layers.size()
            int i = 0
            layer.eachFeature{f ->
                assertNotNull f.geom
                assertNotNull f['state_name']
                i++
            }
            assertEquals 49, i
        }
    }
}



<|start_filename|>src/test/groovy/geoscript/carto/LineItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.assertEquals

class LineItemTest {

    @Test
    void create() {

        LineItem item = new LineItem(10,20,300,400)
            .strokeColor(Color.BLUE)
            .strokeWidth(2)

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(Color.BLUE, item.strokeColor)
        assertEquals(2f, item.strokeWidth, 0.1f)
        assertEquals("LineItem(x = 10, y = 20, width = 300, height = 400, " +
                "stroke-color = java.awt.Color[r=0,g=0,b=255], stroke-width = 2.0)", item.toString())
    }


}


<|start_filename|>src/test/groovy/geoscript/geom/CompoundRingTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.*

/**
 * The CompoundCurve Unit Test
 * @author <NAME>
 */
class CompoundRingTest {

    @Test void createWithRepeatedLineStrings() {
        CompoundRing cc = new CompoundRing(
                new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]),
                new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [10.0, 10.0])
        )
        assertTrue cc.isClosed()
        assertTrue cc.isCurved()
        assertEquals "CompoundRing", cc.geometryType
        assertEquals "COMPOUNDCURVE (CIRCULARSTRING (10.0 10.0, 0.0 20.0, -10.0 10.0), (-10.0 10.0, -10.0 0.0, 10.0 0.0, 10.0 10.0))", cc.curvedWkt
        assertTrue cc.wkt.startsWith("LINEARRING")
        List lines = cc.components
        assertEquals 2, lines.size()
        assertEquals new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]), lines[0]
        assertEquals new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [10.0, 10.0]), lines[1]
        LineString line = cc.linear
        assertTrue line.numPoints > 7
    }

    @Test void createWithListOfLineStrings() {
        CompoundRing cc = new CompoundRing([
                new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]),
                new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [10.0, 10.0])
        ])
        assertTrue cc.isClosed()
        assertTrue cc.isCurved()
        assertEquals "CompoundRing", cc.geometryType
        assertEquals "COMPOUNDCURVE (CIRCULARSTRING (10.0 10.0, 0.0 20.0, -10.0 10.0), (-10.0 10.0, -10.0 0.0, 10.0 0.0, 10.0 10.0))", cc.curvedWkt
        assertTrue cc.wkt.startsWith("LINEARRING")
        List lines = cc.components
        assertEquals 2, lines.size()
        assertEquals new CircularString([10.0, 10.0], [0.0, 20.0], [-10.0, 10.0]), lines[0]
        assertEquals new LineString([-10.0, 10.0], [-10.0, 0.0], [10.0, 0.0], [10.0, 10.0]), lines[1]
        LineString line = cc.linear
        assertTrue line.numPoints > 7
    }

}


<|start_filename|>src/main/groovy/geoscript/render/io/MapReader.groovy<|end_filename|>
package geoscript.render.io

import geoscript.render.Map as GMap

/**
 * Read a Map from a configuration string.
 * @author <NAME>
 */
interface MapReader {

    /**
     * The name of the MapReader
     * @return The name
     */
    String getName()

    /**
     * Read a Map from a configuration string.
     * @param str The string
     * @return A Map
     */
    GMap read(String str)
}

<|start_filename|>src/main/groovy/geoscript/carto/JpegCartoFactory.groovy<|end_filename|>
package geoscript.carto

/**
 * A CartoFactory that creates a CartoBuilder that produces JPEG images.
 * @author <NAME>
 */
class JpegCartoFactory implements CartoFactory {

    @Override
    String getName() {
        "jpeg"
    }

    @Override
    String getMimeType() {
        "image/jpeg"
    }

    @Override
    CartoBuilder create(PageSize pageSize) {
        new ImageCartoBuilder(pageSize, ImageCartoBuilder.ImageType.JPEG)
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/DateTextItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Color
import java.awt.Font

/**
 * Adds date text to a cartographic document.
 * @author <NAME>
 */
class DateTextItem extends Item {

    String format = "MM/dd/yyyy"

    Date date = null

    Color color = Color.BLACK

    Font font = new Font("Arial", Font.PLAIN, 12)

    HorizontalAlign horizontalAlign = HorizontalAlign.LEFT

    VerticalAlign verticalAlign = VerticalAlign.BOTTOM

    /**
     * Create a DateTextItem from the top left
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    DateTextItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the date format string
     * @param format The date format string
     * @return The DateTextItem
     */
    DateTextItem format(String format) {
        this.format = format
        this
    }

    /**
     * Set the Date
     * @param date The Date
     * @return The DateTextItem
     */
    DateTextItem date(Date date) {
        this.date = date
        this
    }

    /**
     * Set the text Color
     * @param color The text Color
     * @return The DateTextItem
     */
    DateTextItem color(Color color) {
        this.color = color
        this
    }

    /**
     * Set the text Font
     * @param font The text Font
     * @return The DateTextItem
     */
    DateTextItem font(Font font) {
        this.font = font
        this
    }

    /**
     * Set the horizontal alignment of the text
     * @param horizontalAlign The HorizontalAlign
     * @return The DateTextItem
     */
    DateTextItem horizontalAlign(HorizontalAlign horizontalAlign) {
        this.horizontalAlign = horizontalAlign
        this
    }

    /**
     * Set the vertical alignment of the text
     * @param verticalAlign The VerticalAlign
     * @return The DateTextItem
     */
    DateTextItem verticalAlign(VerticalAlign verticalAlign) {
        this.verticalAlign = verticalAlign
        this
    }

    @Override
    String toString() {
        "DateTextItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, date = ${date}, format = ${format}, color = ${color}, font = ${font}, horizontal-align = ${horizontalAlign}, vertical-align = ${verticalAlign})"
    }

}


<|start_filename|>examples/naturalearth2mbtiles.groovy<|end_filename|>
import geoscript.layer.ImageTileRenderer
import geoscript.layer.Layer
import geoscript.layer.MBTiles
import geoscript.layer.Pyramid
import geoscript.layer.TileGenerator
import geoscript.layer.TileRenderer
import geoscript.style.Fill
import geoscript.style.Stroke
import geoscript.workspace.Directory
import geoscript.workspace.GeoPackage

import static geoscript.GeoScript.download
import static geoscript.GeoScript.unzip

// Download data from natural earth
File dataDir = new File("naturalearth")
[
        [name: "countries", url: "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip"],
        [name: "ocean",     url: "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/physical/ne_110m_ocean.zip"],
        [name: "places",    url: "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_populated_places.zip"],
        [name: "rivers",    url: "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/physical/ne_110m_rivers_lake_centerlines.zip"],
        [name: "states",    url: "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_1_states_provinces.zip"]

].each { Map item ->
    println "Downloading ${item.name} from ${item.url}..."
    unzip(download(new URL(item.url), new File(dataDir, "${item.name}.zip"), overwrite: false))
}

Directory directory = new Directory("naturalearth")
Layer countries = directory.get("ne_110m_admin_0_countries")
countries.style = new Fill("#ffffff") + new Stroke("#b2b2b2", 0.5)
Layer ocean = directory.get("ne_110m_ocean")
ocean.style = new Fill("#a5bfdd")

File mbtilesFile = new File("world.mbtiles")
if (mbtilesFile.exists()) {
    mbtilesFile.delete()
}
TileGenerator generator = new TileGenerator(verbose: true)
MBTiles mbtiles = new MBTiles(mbtilesFile, "World", "Natural Earth")
ImageTileRenderer renderer = new ImageTileRenderer(mbtiles, [ocean, countries])
generator.generate(mbtiles, renderer, 0, 3)


<|start_filename|>src/test/groovy/geoscript/feature/io/StringSchemaWriterTest.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Schema
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The StringSchemaWriter Unit Test
 * @author <NAME>
 */
class StringSchemaWriterTest {

    @Test void write() {
        Schema schema = new Schema("points", "geom:Point:srid=4326,name:String,price:float")
        SchemaWriter writer = new StringSchemaWriter()
        String str = writer.write(schema)
        assertEquals "geom:Point:srid=4326,name:String,price:Float", str
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/SvgCartoBuilderTest.groovy<|end_filename|>
package geoscript.carto

import geoscript.FileUtil
import geoscript.layer.GeoTIFF
import geoscript.layer.Shapefile
import geoscript.render.Map
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import java.awt.*

class SvgCartoBuilderTest {

    @TempDir
    File folder

    @Test
    void build() {

        Map map = new Map(layers: [
            new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        ])

        Map overViewMap = new Map(layers: [
            new GeoTIFF(new File(getClass().getClassLoader().getResource("raster.tif").toURI())).read()
        ])

        boolean saveToTarget = false
        File file = saveToTarget ? new File("target/map.svg") : new File(folder, "map.svg")
        file.withOutputStream { OutputStream outputStream ->
            new SvgCartoBuilder(PageSize.LETTER_LANDSCAPE)
                .rectangle(new RectangleItem(0, 0, 792, 612).strokeColor(Color.WHITE).fillColor(Color.WHITE))
                .grid(new GridItem(0,0,792, 612))
                .rectangle(new RectangleItem(10, 10, 772, 592))
                .rectangle(new RectangleItem(20, 20, 752, 80))
                .text(new TextItem(30, 50, 200, 20).text("Map Title").font(new Font("Arial", Font.BOLD, 36)))
                .dateText(new DateTextItem(30, 85, 200, 10).font(new Font("Arial", Font.ITALIC, 18)))
                .paragraph(new ParagraphItem(250, 30, 380, 70).text("""Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
""").font(new Font("Arial", Font.PLAIN, 8)))
                .line(new LineItem(710, 30, 1, 60))
                .image(new ImageItem(640, 30, 60, 60).path(new File(getClass().getClassLoader().getResource("image.png").toURI())))
                .northArrow(new NorthArrowItem(720, 30, 40, 60))
                .rectangle(new RectangleItem(20, 110, 752, 480))
                .map(new MapItem(30, 120, 742, 470).map(map))
                .overViewMap(new OverviewMapItem(30, 490, 100, 90).overviewMap(overViewMap).linkedMap(map))
                .overViewMap(new OverviewMapItem(150, 490, 100, 90).overviewMap(overViewMap).linkedMap(map).zoomIntoBounds(true).scaleFactor(3.0))
                .table(new TableItem(460, 120, 300, 200)
                    .columns(["ID","Name"])
                    .row([[ID: 1, Name: "One"]])
                    .row([[ID: 2, Name: "Two"]])
                    .row([[ID: 3, Name: "Three"]])
                )
                .legend(new LegendItem(640, 500, 120,80).addMap(map))
                .scaleText(new ScaleTextItem(150, 85, 200, 10).map(map).font(new Font("Arial", Font.ITALIC, 18)))
                .scaleBar(new ScaleBarItem(50, 125, 200, 20).map(map))
                .build(outputStream)
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/GridItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.assertEquals

class GridItemTest {

    @Test
    void create() {

        GridItem item = new GridItem(10,20,300,400)
            .strokeColor(Color.BLUE)
            .strokeWidth(2)
            .size(20)

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(20, item.size)
        assertEquals(Color.BLUE, item.strokeColor)
        assertEquals(2f, item.strokeWidth, 0.1f)
        assertEquals("GridItem(x = 10, y = 20, width = 300, height = 400, size = 20, " +
                "stroke-color = java.awt.Color[r=0,g=0,b=255], stroke-width = 2.0)", item.toString())
    }


}


<|start_filename|>src/test/groovy/geoscript/carto/ImageItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class ImageItemTest {

    @Test
    void create() {

        ImageItem item = new ImageItem(10,20,300,400)
            .path(new File(getClass().getClassLoader().getResource("image.png").toURI()))

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals("image.png", item.path.name)
        assertTrue(item.toString().contains("ImageItem(x = 10, y = 20, width = 300, height = 400, path = "))
        assertTrue(item.toString().endsWith("image.png)"))
    }


}


<|start_filename|>src/test/groovy/geoscript/layer/GeneratingTileLayerTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.FileUtil
import geoscript.layer.io.MvtWriter
import geoscript.style.Fill
import geoscript.style.Stroke
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The GeneratingTileLayer unit test
 * @author <NAME>
 */
class GeneratingTileLayerTest {

    @TempDir
    private File folder

    private void generateOnDemand(TileLayer tileLayer, TileRenderer tileRenderer) {
        GeneratingTileLayer generatingTileLayer = new GeneratingTileLayer(tileLayer, tileRenderer)
        [
                [z: 0, x: 0, y: 0],
                [z: 1, x: 0, y: 1],
                [z: 9, x: 8, y: 10]
        ].each { Map tile ->
            assertNull tileLayer.get(tile.z, tile.x, tile.y).data
            assertNotNull generatingTileLayer.get(tile.z, tile.x, tile.y).data
            assertNotNull tileLayer.get(tile.z, tile.x, tile.y).data
        }
        generatingTileLayer.close()
    }

    @Test void generateMbtiles() {
        Layer layer = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        layer.style = new Fill("wheat") + new Stroke("navy", 0.1)
        File file = new File(folder, "states.mbtiles")
        TileLayer tileLayer = new MBTiles(file, "states", "A map of the united states")
        ImageTileRenderer tileRenderer = new ImageTileRenderer(tileLayer, layer)
        generateOnDemand(tileLayer, tileRenderer)
    }

    @Test void generateGeoPackage() {
        Layer layer = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        layer.style = new Fill("wheat") + new Stroke("navy", 0.1)
        File file = new File(folder, "states.gpkg")
        TileLayer tileLayer = new GeoPackage(file, "states", Pyramid.createGlobalMercatorPyramid())
        ImageTileRenderer tileRenderer = new ImageTileRenderer(tileLayer, layer)
        generateOnDemand(tileLayer, tileRenderer)
    }

    @Test void generateMvt() {
        File dir = FileUtil.createDir(folder, "states")
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        TileLayer tileLayer = new VectorTiles("states", dir, pyramid, "mvt")
        geoscript.layer.io.Writer writer = new MvtWriter()
        Layer layer = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        List fields = [layer.schema.get("STATE_NAME")]
        TileRenderer tileRenderer = new VectorTileRenderer(writer, layer, fields)
        generateOnDemand(tileLayer, tileRenderer)
    }

}


<|start_filename|>src/test/groovy/geoscript/render/RenderersTest.groovy<|end_filename|>
package geoscript.render

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The Renderers Unit Test
 * @author <NAME>
 */
class RenderersTest {

    @Test void list() {
        List<Renderer> renderers = Renderers.list()
        assertNotNull renderers
        assertTrue renderers.size() > 0
    }

    @Test void find() {
        Renderer renderer = Renderers.find("png")
        assertNotNull renderer
        renderer = Renderers.find("jpeg")
        assertNotNull renderer

        renderer = Renderers.find("asdf")
        assertNull renderer
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/GeoPackageTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import geoscript.proj.Projection
import org.apache.commons.dbcp.BasicDataSource
import org.geotools.image.test.ImageAssert
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import static geoscript.AssertUtil.assertBoundsEquals

/**
 * The GeoPackage Unit Test
 * @author <NAME>
 */
class GeoPackageTest {

    @TempDir
    private File folder

    @Test
    void create() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Bounds b = new Bounds(-2.00363951478813E7, -2.00374712051371E7, 2.00363951478813E7, 2.00374712051371E7, "EPSG:3857")
        assertEquals "states", layer.name
        assertEquals new Projection("EPSG:3857"), layer.proj
        assertBoundsEquals b, layer.bounds, 0.0001
        Pyramid pyramid = layer.pyramid
        assertEquals "EPSG:3857", pyramid.proj.id
        assertBoundsEquals b, pyramid.bounds, 0.0001
        assertEquals 256, pyramid.tileWidth
        assertEquals 256, pyramid.tileHeight
        assertEquals Pyramid.Origin.TOP_LEFT, pyramid.origin
        assertEquals 20, pyramid.grids.size()
        pyramid.grids.eachWithIndex { Grid g, int z ->
            assertEquals z, g.z
            int n = Math.pow(2, z)
            assertEquals n, g.width
            assertEquals n, g.height
            assertEquals 156412.0 / n, g.xResolution, 0.01
            assertEquals 156412.0 / n, g.yResolution, 0.01
        }
        layer.close()
    }

    @Test
    void createFromDataSource() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        BasicDataSource ds = new BasicDataSource()
        ds.driverClassName = "org.sqlite.JDBC"
        ds.url = "jdbc:sqlite:" + file.absolutePath
        GeoPackage layer = new GeoPackage(ds, "states")
        Bounds b = new Bounds(-2.00363951478813E7, -2.00374712051371E7, 2.00363951478813E7, 2.00374712051371E7, "EPSG:3857")
        assertEquals "states", layer.name
        assertEquals new Projection("EPSG:3857"), layer.proj
        assertBoundsEquals b, layer.bounds, 0.0001
        Pyramid pyramid = layer.pyramid
        assertEquals "EPSG:3857", pyramid.proj.id
        assertBoundsEquals b, pyramid.bounds, 0.0001
        assertEquals 256, pyramid.tileWidth
        assertEquals 256, pyramid.tileHeight
        assertEquals Pyramid.Origin.TOP_LEFT, pyramid.origin
        assertEquals 20, pyramid.grids.size()
        pyramid.grids.eachWithIndex { Grid g, int z ->
            assertEquals z, g.z
            int n = Math.pow(2, z)
            assertEquals n, g.width
            assertEquals n, g.height
            assertEquals 156412.0 / n, g.xResolution, 0.01
            assertEquals 156412.0 / n, g.yResolution, 0.01
        }
        layer.close()
    }

    @Test
    void createNew() {
        File file = new File(folder, "states.gpkg")
        file.delete()
        GeoPackage layer = new GeoPackage(file, "states", Pyramid.createGlobalMercatorPyramid())
        Bounds b = new Bounds(-2.00363951478813E7, -2.00374712051371E7, 2.00363951478813E7, 2.00374712051371E7, "EPSG:3857")
        assertEquals "states", layer.name
        assertEquals new Projection("EPSG:3857"), layer.proj
        assertBoundsEquals b, layer.bounds, 0.0001
        Pyramid pyramid = layer.pyramid
        assertEquals "EPSG:3857", pyramid.proj.id
        assertBoundsEquals b, pyramid.bounds, 0.0001
        assertEquals 256, pyramid.tileWidth
        assertEquals 256, pyramid.tileHeight
        assertEquals Pyramid.Origin.TOP_LEFT, pyramid.origin
        assertEquals 20, pyramid.grids.size()
        pyramid.grids.eachWithIndex { Grid g, int z ->
            assertEquals z, g.z
            int n = Math.pow(2, z)
            assertEquals n, g.width
            assertEquals n, g.height
            assertEquals 156412.0 / n, g.xResolution, 0.01
            assertEquals 156412.0 / n, g.yResolution, 0.01
        }
        layer.close()
    }

    @Test
    void get() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Tile tile = layer.get(4, 2, 3)
        assertNotNull tile
        assertEquals 4, tile.z
        assertEquals 2, tile.x
        assertEquals 3, tile.y
        assertNotNull tile.data
        layer.close()
    }

    @Test
    void put() {
        // Since we are modifying the mbtiles file copy it to a temp file
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        File newFile = new File(folder, "states_temp1.gpkg")
        newFile.withOutputStream { out ->
            file.withInputStream { inp ->
                out << inp
            }
        }
        GeoPackage layer = new GeoPackage(newFile, "states")

        // Make sure Tile doesn't exist in database
        Tile tile = layer.get(10, 0, 0)
        assertNotNull tile
        assertEquals 10, tile.z
        assertEquals 0, tile.x
        assertEquals 0, tile.y
        assertNull tile.data

        // Load a tile image
        File f = new File(getClass().getClassLoader().getResource("0.png").toURI())
        tile.data = f.bytes
        assertNotNull tile.data

        // Save Tile and make sure it saved correctly by getting it again
        layer.put(tile)
        tile = layer.get(10, 0, 0)
        assertNotNull tile
        assertEquals 10, tile.z
        assertEquals 0, tile.x
        assertEquals 0, tile.y
        assertNotNull tile.data
        layer.close()
    }

    @Test
    void delete() {
        // Since we are modifying the mbtiles file copy it to a temp file
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        File newFile = new File(folder, "states_temp2.gpkg")
        newFile.withOutputStream { out ->
            file.withInputStream { inp ->
                out << inp
            }
        }
        GeoPackage layer = new GeoPackage(newFile, "states")
        Tile tile = layer.get(4, 2, 3)
        assertNotNull tile
        assertNotNull tile.data
        layer.delete(tile)
        tile = layer.get(4, 2, 3)
        assertNotNull tile
        assertNull tile.data
        layer.close()
    }

    @Test
    void deleteTiles() {
        // Since we are modifying the geopackage file copy it to a temp file
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        File newFile = new File(folder, "states_temp2.gpkg")
        newFile.withOutputStream { out ->
            file.withInputStream { inp ->
                out << inp
            }
        }
        GeoPackage layer = new GeoPackage(newFile, "states")
        layer.tiles(4).each { Tile tile ->
            assertNotNull tile
            assertNotNull tile.data
        }
        layer.delete(layer.tiles(4))
        layer.tiles(4).each { Tile tile ->
            assertNotNull tile
            assertNull tile.data
        }
        layer.tiles(3).each { Tile tile ->
            assertNotNull tile
            assertNotNull tile.data
        }
        layer.close()
    }

    @Test
    void tilesByZoomLevel() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        TileCursor cursor = layer.tiles(1)
        assertEquals 1, cursor.z
        assertEquals 0, cursor.minX
        assertEquals 0, cursor.minY
        assertEquals 1, cursor.maxX
        assertEquals 1, cursor.maxY
        assertEquals 2, cursor.width
        assertEquals 2, cursor.height
        assertEquals 4, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 1, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 4, c
        layer.close()
    }

    @Test
    void tilesByTileCoordinates() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        TileCursor cursor = layer.tiles(2, 1, 2, 3, 3)
        assertEquals 2, cursor.z
        assertEquals 1, cursor.minX
        assertEquals 2, cursor.minY
        assertEquals 3, cursor.maxX
        assertEquals 3, cursor.maxY
        assertEquals 3, cursor.width
        assertEquals 2, cursor.height
        assertEquals 6, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 2, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 6, c
        layer.close()
    }

    @Test
    void tilesByBoundsAndZoomLevel() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Bounds b = new Bounds(-123.09, 46.66, -121.13, 47.48, "EPSG:4326").reproject("EPSG:3857")
        TileCursor cursor = layer.tiles(b, 3)
        assertEquals 3, cursor.z
        assertEquals 1, cursor.minX
        assertEquals 2, cursor.minY
        assertEquals 1, cursor.maxX
        assertEquals 2, cursor.maxY
        assertEquals 1, cursor.width
        assertEquals 1, cursor.height
        assertEquals 1, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 3, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 1, c
        layer.close()
    }

    @Test
    void tilesByBoundsAndResolutions() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        TileCursor cursor = layer.tiles(b, b.width / 400, b.height / 300)
        assertEquals 4, cursor.z
        assertEquals 2, cursor.minX
        assertEquals 5, cursor.minY
        assertEquals 5, cursor.maxX
        assertEquals 6, cursor.maxY
        assertEquals 4, cursor.width
        assertEquals 2, cursor.height
        assertEquals 8, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 4, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 8, c
        layer.close()
    }

    @Test
    void tilesByBoundsAndImageSize() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        TileCursor cursor = layer.tiles(b, 400, 300)
        assertEquals 4, cursor.z
        assertEquals 2, cursor.minX
        assertEquals 5, cursor.minY
        assertEquals 5, cursor.maxX
        assertEquals 6, cursor.maxY
        assertEquals 4, cursor.width
        assertEquals 2, cursor.height
        assertEquals 8, cursor.size
        int c = 0
        cursor.each { Tile t ->
            assertEquals 4, t.z
            assertNotNull t.data
            c++
        }
        assertEquals 8, c
        layer.close()
    }

    @Test
    void getTileCoordinates() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        Map coords = layer.getTileCoordinates(b, layer.pyramid.grid(4))
        assertEquals 2, coords.minX
        assertEquals 5, coords.minY
        assertEquals 5, coords.maxX
        assertEquals 6, coords.maxY
        layer.close()
    }

    @Test
    void getRaster() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        Raster raster = layer.getRaster(layer.tiles(b, 4))
        assertNotNull raster
        ImageAssert.assertEquals(getFile("geoscript/layer/gpkg_raster.png"), raster.image, 100)
        File out = new File(folder, "raster.png")
        WorldImage format = new WorldImage(out)
        format.write(raster)
        assertTrue out.exists()
        assertTrue out.length() > 0
        layer.close()
    }

    @Test
    void getRasterCropped() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        Bounds b = new Bounds(-124.73142200000001, 24.955967, -66.969849, 49.371735, "EPSG:4326").reproject("EPSG:3857")
        Raster raster = layer.getRaster(b, 400, 300)
        assertNotNull raster
        ImageAssert.assertEquals(getFile("geoscript/layer/gpkg_raster_cropped.png"), raster.image, 100)
        File out = new File(folder, "raster.png")
        WorldImage format = new WorldImage(out)
        format.write(raster)
        assertTrue out.exists()
        assertTrue out.length() > 0
        layer.close()
    }

    @Test
    void tileCounts() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        List stats = layer.tileCounts
        stats.eachWithIndex { Map stat, int index ->
            assertEquals(index, stat.zoom)
            assertEquals(Math.pow(4, index), stat.tiles, 0.1)
            assertEquals(Math.pow(4, index), stat.total, 0.1)
            assertEquals(1.0, stat.percent, 0.1)
        }
    }

    @Test
    void minMaxZoom() {
        File file = new File(getClass().getClassLoader().getResource("states.gpkg").toURI())
        GeoPackage layer = new GeoPackage(file, "states")
        assertEquals 0, layer.minZoom
        assertEquals 4, layer.maxZoom
    }

    @Test
    void deleteLayer() {
        File file = new File(folder, "states.gpkg")
        file.delete()
        GeoPackage layer1 = new GeoPackage(file, "states", Pyramid.createGlobalMercatorPyramid())
        GeoPackage layer2 = new GeoPackage(file, "world", Pyramid.createGlobalGeodeticPyramid())
        assertEquals(["states","world"], GeoPackage.getNames(file))
        layer1.delete()
        assertEquals(["world"], GeoPackage.getNames(file))
        layer2.delete()
        assertEquals([], GeoPackage.getNames(file))
    }

    private File getFile(String resource) {
        new File(getClass().getClassLoader().getResource(resource).toURI())
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/io/Gml2ReaderTest.groovy<|end_filename|>
package geoscript.geom.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.*

/**
 * The GML2Reader UnitTest
 * @author <NAME>
 */
class Gml2ReaderTest {

    @Test void readPoint() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = "<gml:Point><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point>"
        Point expected = new Point(111,-47)
        Point actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = "<gml:Point xmlns:gml=\"http://www.opengis.net/gml\"><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point>"
        expected = new Point(111,-47)
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readLineString() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = "<gml:LineString><gml:coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0</gml:coordinates></gml:LineString>"
        LineString expected = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        LineString actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = "<gml:LineString xmlns:gml=\"http://www.opengis.net/gml\"><gml:coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0</gml:coordinates></gml:LineString>"
        expected = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readLinearRing() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = "<gml:LinearRing><gml:coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0 111.0,-47.0</gml:coordinates></gml:LinearRing>"
        LinearRing expected = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        LinearRing actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = "<gml:LinearRing xmlns:gml=\"http://www.opengis.net/gml\"><gml:coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0 111.0,-47.0</gml:coordinates></gml:LinearRing>"
        expected = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPolygon() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = "<gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>2.0,2.0 4.0,2.0 4.0,4.0 2.0,4.0 2.0,2.0</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>5.0,5.0 6.0,5.0 6.0,6.0 5.0,6.0 5.0,5.0</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs></gml:Polygon>"
        Polygon expected = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        Polygon actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = "<gml:Polygon xmlns:gml=\"http://www.opengis.net/gml\"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>2.0,2.0 4.0,2.0 4.0,4.0 2.0,4.0 2.0,2.0</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>5.0,5.0 6.0,5.0 6.0,6.0 5.0,6.0 5.0,5.0</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs></gml:Polygon>"
        expected = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPoint() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = "<gml:MultiPoint><gml:pointMember><gml:Point><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point></gml:pointMember><gml:pointMember><gml:Point><gml:coordinates>110.0,-46.5</gml:coordinates></gml:Point></gml:pointMember></gml:MultiPoint>"
        MultiPoint expected = new MultiPoint([111,-47],[110,-46.5])
        MultiPoint actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = "<gml:MultiPoint xmlns:gml=\"http://www.opengis.net/gml\"><gml:pointMember><gml:Point><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point></gml:pointMember><gml:pointMember><gml:Point><gml:coordinates>110.0,-46.5</gml:coordinates></gml:Point></gml:pointMember></gml:MultiPoint>"
        expected = new MultiPoint([111,-47],[110,-46.5])
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiLineString() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = """<gml:MultiLineString><gml:lineStringMember><gml:LineString><gml:coordinates>1.0,2.0 3.0,4.0</gml:coordinates></gml:LineString></gml:lineStringMember><gml:lineStringMember><gml:LineString><gml:coordinates>5.0,6.0 7.0,8.0</gml:coordinates></gml:LineString></gml:lineStringMember></gml:MultiLineString>"""
        MultiLineString expected = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        MultiLineString actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = """<gml:MultiLineString xmlns:gml=\"http://www.opengis.net/gml\"><gml:lineStringMember><gml:LineString><gml:coordinates>1.0,2.0 3.0,4.0</gml:coordinates></gml:LineString></gml:lineStringMember><gml:lineStringMember><gml:LineString><gml:coordinates>5.0,6.0 7.0,8.0</gml:coordinates></gml:LineString></gml:lineStringMember></gml:MultiLineString>"""
        expected = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPolygon() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = "<gml:MultiPolygon><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.0,2.0 3.0,4.0 5.0,6.0 1.0,2.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>7.0,8.0 9.0,10.0 11.0,12.0 7.0,8.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon>"
        MultiPolygon expected = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        MultiPolygon actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = "<gml:MultiPolygon xmlns:gml=\"http://www.opengis.net/gml\"><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.0,2.0 3.0,4.0 5.0,6.0 1.0,2.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>7.0,8.0 9.0,10.0 11.0,12.0 7.0,8.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon>"
        expected = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readGeometryCollection() {
        Gml2Reader reader = new Gml2Reader()

        // Without XML Namespace
        String gml = "<gml:GeometryCollection><gml:geometryMember><gml:Point><gml:coordinates>100.0,0.0</gml:coordinates></gml:Point></gml:geometryMember><gml:geometryMember><gml:LineString><gml:coordinates>101.0,0.0 102.0,1.0</gml:coordinates></gml:LineString></gml:geometryMember></gml:GeometryCollection>"
        GeometryCollection expected = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        GeometryCollection actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt

        // With XML Namespace
        gml = "<gml:GeometryCollection xmlns:gml=\"http://www.opengis.net/gml\"><gml:geometryMember><gml:Point><gml:coordinates>100.0,0.0</gml:coordinates></gml:Point></gml:geometryMember><gml:geometryMember><gml:LineString><gml:coordinates>101.0,0.0 102.0,1.0</gml:coordinates></gml:LineString></gml:geometryMember></gml:GeometryCollection>"
        expected = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        actual = reader.read(gml)
        assertEquals expected.wkt, actual.wkt
    }
}



<|start_filename|>src/test/groovy/geoscript/carto/RectangleItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.assertEquals

class RectangleItemTest {

    @Test
    void create() {

        RectangleItem item = new RectangleItem(10,20,300,400)
            .fillColor(Color.WHITE)
            .strokeColor(Color.BLUE)
            .strokeWidth(1.2)

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(Color.BLUE, item.strokeColor)
        assertEquals(1.2f, item.strokeWidth, 0.1f)
        assertEquals(Color.WHITE, item.fillColor)
        assertEquals("RectangleItem(x = 10, y = 20, width = 300, height = 400, " +
                "stroke-color = java.awt.Color[r=0,g=0,b=255], " +
                "fill-color = java.awt.Color[r=255,g=255,b=255], " +
                "stroke-width = 1.2)", item.toString())
    }


}


<|start_filename|>src/main/groovy/geoscript/carto/PdfCartoBuilder.groovy<|end_filename|>
package geoscript.carto

import com.lowagie.text.Document
import com.lowagie.text.Rectangle
import com.lowagie.text.pdf.PdfContentByte
import com.lowagie.text.pdf.PdfWriter
import org.apache.commons.io.IOUtils

import java.awt.Graphics2D

/**
 * Build a cartographic document as a PDF document
 * @author <NAME>
 */
class PdfCartoBuilder implements CartoBuilder {

    private final Document document

    private final Java2DCartoBuilder java2DCartoBuilder

    private final ByteArrayOutputStream outputStream

    /**
     * Create a PdfCartoBuilder with a landscape letter page size
     */
    PdfCartoBuilder() {
        this(PageSize.LETTER_LANDSCAPE)
    }

    /**
     * Create a PdfCartoBuilder with the given PageSize
     * @param pageSize The PageSize
     */
    PdfCartoBuilder(PageSize pageSize) {
        this.document = new Document(new Rectangle(pageSize.width as float, pageSize.height as float))
        this.outputStream = new ByteArrayOutputStream()
        PdfWriter writer = PdfWriter.getInstance(document, outputStream)
        document.open()
        PdfContentByte cb = writer.getDirectContent()
        Graphics2D g2 = cb.createGraphicsShapes(document.pageSize.width, document.pageSize.height)
        this.java2DCartoBuilder = new Java2DCartoBuilder(g2, pageSize)
    }

    @Override
    CartoBuilder dateText(DateTextItem dateTextItem) {
        java2DCartoBuilder.dateText(dateTextItem)
        this
    }

    @Override
    CartoBuilder image(ImageItem imageItem) {
        java2DCartoBuilder.image(imageItem)
        this
    }

    @Override
    CartoBuilder line(LineItem lineItem) {
        java2DCartoBuilder.line(lineItem)
        this
    }

    @Override
    CartoBuilder map(MapItem mapItem) {
        java2DCartoBuilder.map(mapItem)
        this
    }

    @Override
    CartoBuilder northArrow(NorthArrowItem northArrowItem) {
        java2DCartoBuilder.northArrow(northArrowItem)
        this
    }

    @Override
    CartoBuilder text(TextItem textItem) {
        java2DCartoBuilder.text(textItem)
        this
    }

    @Override
    CartoBuilder paragraph(ParagraphItem paragraphItem) {
        java2DCartoBuilder.paragraph(paragraphItem)
        this
    }

    @Override
    CartoBuilder rectangle(RectangleItem rectangleItem) {
        java2DCartoBuilder.rectangle(rectangleItem)
        this
    }

    @Override
    CartoBuilder scaleText(ScaleTextItem scaleTextItem) {
        java2DCartoBuilder.scaleText(scaleTextItem)
        this
    }

    @Override
    CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {
        java2DCartoBuilder.scaleBar(scaleBarItem)
        this
    }

    @Override
    CartoBuilder grid(GridItem gridItem) {
        java2DCartoBuilder.grid(gridItem)
        this
    }

    @Override
    CartoBuilder overViewMap(OverviewMapItem overviewMapItem) {
        java2DCartoBuilder.overViewMap(overviewMapItem)
        this
    }

    @Override
    CartoBuilder table(TableItem tableItem) {
        java2DCartoBuilder.table(tableItem)
        this
    }

    @Override
    CartoBuilder legend(LegendItem legendItem) {
        java2DCartoBuilder.legend(legendItem)
        this
    }

    @Override
    void build(OutputStream outputStream) {
        java2DCartoBuilder.graphics.dispose()
        document.close()
        this.outputStream.writeTo(outputStream)
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/NorthArrowItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.*

class NorthArrowItemTest {

    @Test
    void create() {

        NorthArrowItem item = new NorthArrowItem(10,20,300,400)
            .fillColor1(Color.WHITE)
            .strokeColor1(Color.BLUE)
            .fillColor2(Color.YELLOW)
            .strokeColor2(Color.RED)
            .strokeWidth(1.2)
            .drawText(true)
            .font(new Font("Arial", Font.BOLD, 52))
            .textColor(Color.BLUE)

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(Color.BLUE, item.strokeColor1)
        assertEquals(Color.WHITE, item.fillColor1)
        assertEquals(Color.RED, item.strokeColor2)
        assertEquals(Color.YELLOW, item.fillColor2)
        assertEquals(1.2f, item.strokeWidth, 0.1f)
        assertTrue(item.drawText)
        assertEquals(Color.BLUE, item.textColor)
        assertTrue(item.toString().startsWith("NorthArrowItem(x = 10, y = 20, width = 300, height = 400, " +
                "fill-color1 = java.awt.Color[r=255,g=255,b=255], stroke-color1 = java.awt.Color[r=0,g=0,b=255], " +
                "fill-color2 = java.awt.Color[r=255,g=255,b=0], stroke-color2 = java.awt.Color[r=255,g=0,b=0], " +
                "stroke-width = 1.2"))
    }


}


<|start_filename|>src/main/groovy/geoscript/workspace/Geobuf.groovy<|end_filename|>
package geoscript.workspace

import org.geotools.data.DataStore
import org.geotools.data.geobuf.GeobufDirectoryDataStore
import org.geotools.data.geobuf.GeobufDataStoreFactory

/**
 * A Geobuf Workspace.
 * @author <NAME>
 */
class Geobuf extends Workspace {

    /**
     * The directory of pbf files
     */
    private File directory

    /**
     * The maximum precision
     */
    private int precision

    /**
     * The supported geometry coordinate dimension
     */
    private int dimension

    /**
     * Create a new Geobuf Workspace
     * @param options Optional named parameters:
     * <ul>
     *     <li> precision = The maximum precision (defaults to 6) </li>
     *     <li> dimension = The supported geometry coordinates dimension (defaults to 2) </li>
     * </ul>
     * @param directory The directory of pbf files
     */
    Geobuf(Map options = [:], File directory) {
        this(createDataStore(directory, options.get("precision", 6), options.get("dimension", 2)))
        this.directory = directory
        this.precision = precision
        this.dimension = dimension
    }

    /**
     * Create a new Geobuf Workspace from a GeobufDirectoryDataStore
     * @param ds The GeobufDirectoryDataStore
     */
    Geobuf(GeobufDirectoryDataStore ds) {
        super(ds)
    }

    /**
     * Get the format
     * @return The workspace format name
     */
    @Override
    String getFormat() {
        return "Geobuf"
    }

    /**
     * Remove a Layer by name from this Workspace
     * @param name The Layer name
     */
    @Override
    void remove(String name) {
        File file = new File(directory, name.endsWith(".pbf") ? name : "${name}.pbf")
        if (file.exists()) {
            file.delete()
        }
    }

    private static DataStore createDataStore(File directory, int precision, int dimension) {
        Map params = new java.util.HashMap()
        params.put("file", directory.absolutePath)
        params.put("precision", precision)
        params.put("dimension", dimension)
        GeobufDataStoreFactory factory = new GeobufDataStoreFactory()
        factory.createDataStore(params)
    }

    /**
     * The Geobuf WorkspaceFactory
     */
    static class Factory extends WorkspaceFactory<Geobuf> {

        @Override
        Map getParametersFromString(String str) {
            Map params = [:]
            if (!str.contains("=") && str.endsWith(".pbf")) {
                File file = new File(str).absoluteFile
                if (!file.isDirectory()) {
                    file = file.parentFile
                }
                params.put("type", "geobuf")
                params.put("file", file.absolutePath)
            } else {
                params = super.getParametersFromString(str)
            }
            params
        }

        @Override
        Geobuf create(String type, Map params) {
            if (type.equalsIgnoreCase('geobuf') && params.containsKey('file')) {
                File file = params.get('file') instanceof File ? params.get('file') : new File(params.get('file'))
                if (!file.isDirectory()) {
                    file = file.parentFile
                }
                new Geobuf(file)
            } else {
                null
            }
        }

        @Override
        Geobuf create(DataStore dataStore) {
            if (dataStore != null && dataStore instanceof org.geotools.data.geobuf.GeobufDirectoryDataStore) {
                new Geobuf(dataStore)
            } else {
                null
            }
        }
    }
}


<|start_filename|>src/main/groovy/geoscript/feature/Feature.groovy<|end_filename|>
package geoscript.feature

import org.locationtech.jts.geom.Geometry as JtsGeometry
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.layer.Layer
import org.geotools.feature.simple.SimpleFeatureBuilder
import org.opengis.feature.simple.SimpleFeature

/**
 * A Feature contains a set of named attributes with values.
 * <p>A Feature is created from a Map of name value pairs and an identifier.</p>
 * <p><i>Without a {@link Schema} (see below) the data types are inferred).</i></p>
 * <p><blockquote><pre>
 * Feature f = new Feature(['name': 'anvil', 'price': 100.0], 'widgets.1')
 * </pre></blockquote></p>
 * <p>A Feature can also be created from a list of values, an identifier, and a {@link Schema}</p>
 * <p><blockquote><pre>
 * {@link Schema} s = new {@link Schema}('widgets', [['name','string'],['price','float']])
 * Feature f = new Feature(['anvil', 100.0], '1', s)
 * </pre></blockquote></p>
 * <p>A Feature can also be created from a Map of name value pairs, an identifier, and a {@link Schema}</p>
 * <p><blockquote><pre>
 * {@link Schema} s = new {@link Schema}('widgets', [['name','string'],['price','float']])
 * Feature f = new Feature(['name': 'anvil', 'price': 100.0], '1', s)
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Feature {

    /**
     * The wrapped GeoTools SimpleFeature
     */
    SimpleFeature f

    /**
     * The Schema
     */
    Schema schema

    /**
     * The Layer the Feature was read from
     */
    Layer layer

    /**
     * Create a new Feature by wrapping a GeoTools SimpleFeature
     * @param f The GeoTools SimpleFeature
     */
    Feature(SimpleFeature f) {
        this.f = f
        this.schema = new Schema(this.f.featureType)
    }

    /**
     * Create a new Feature with a Map of attributes, an id, and a Schema.
     * <p><code>
     * Schema s = new Schema('widgets', [['name','string'],['price','float']])
     * Feature f = new Feature(['name': 'anvil', 'price': 100.0], '1', s)
     * </code></p>
     * @param attributes A Map of name value pairs
     * @param id A String identifier
     * @param schema The Schema
     */
    Feature(Map attributes, String id, Schema schema) {
        this(buildFeature(attributes, id, schema))
    }

    /**
     * Create a new Feature with a List of values, an id, and a Schema.  The
     * List of values must be in the same order as the Schema's fields.
     * <p><code>
     * Schema s = new Schema('widgets', [['name','string'],['price','float']])
     * Feature f = new Feature(['anvil', 100.0], '1', s)
     * </code></p>
     * @param attributes A List of attribute values
     * @param id A String identifier
     * @param schema The Schema
     */
    Feature(List attributes, String id, Schema schema) {
        this(buildFeature(attributes, id, schema))
    }

    /**
     * Create a new Feature with a Map of Attributes and an Id.  The Schema is
     * inferred from the attribute values.
     * <p><code>
     * Feature f = new Feature(['name': 'anvil', 'price': 100.0], 'widgets.1')
     * </code></p>
     * @param atributes A Map of name value pairs
     * @param id The string identifer
     */
    Feature(Map attributes, String id) {
        this(buildFeature(attributes, id))
    }

    /**
     * Get the Feature's ID
     * @return The Feature's ID
     */
    String getId() {
        f.identifier.toString()
    }

    /**
     * Get the Feature's Geometry
     * @return The Feature's Geometry
     */
    Geometry getGeom() {
        Geometry.wrap((JtsGeometry) f.defaultGeometry)
    }

    /**
     * Set the Feature's Geometry
     * @param geom The new Geometry
     */
    void setGeom(Geometry geom) {
        f.defaultGeometry = geom.g
    }

    /**
     * The Bounds of the Feature's Geometry.
     * @param The Bounds of the Feature's Geometry
     */
    Bounds getBounds() {
        Bounds b = getGeom().bounds
        new Bounds(b.minX, b.minY, b.maxX, b.maxY, schema.proj)
    }

    /**
     * Get a value by Field name.
     * <p><code>String name = feature.get("name")</code></p>
     * @param name The Field name
     * @return The attribute value
     */
    Object get(String name) {
        if (name != null && !schema.has(name) && name.length() >= 10) {
            name = name.substring(0,10)
        }
        Object obj = f.getAttribute(name)
        if (obj instanceof JtsGeometry) {
            return Geometry.wrap((JtsGeometry)obj)
        }
        else {
            return obj
        }
    }

    /**
     * Get a value by a Field.
     * <p><code>Field fld = feature.schema.get("name")</code></p>
     * <p><code>String name = feature.get(fld)</code></p>
     * @param name The Field name
     * @return The attribute value
     */
    Object get(Field field) {
        get(field.name)
    }

    /**
     * Get a value by Field name.  This method supports
     * a the following syntax:
     * <p><code>String name = feature["name"]</code></p>
     * @param name The Field name
     * @return The attribute value
     */
    Object getAt(String name) {
        get(name)
    }

    /**
     * Get a value by Field.  This method supports
     * a the following syntax:
     * <p><code>Field fld = feature.schema.get("name")</code></p>
     * <p><code>String name = feature[fld]</code></p>
     * @param name The Field
     * @return The attribute value
     */
    Object getAt(Field field) {
        get(field)
    }

    /**
     * Set a value.
     * <p><code>feature.set("name") = "lighthouse"</code></p>
     * @param name The Field name
     * @param value The new attribute value
     */
    void set(String name, Object value) {
        if (name != null && !schema.has(name) && name.length() >= 10) {
            name = name.substring(0,10)
        }
        if (name.equalsIgnoreCase(schema.geom.name)) {
            f.defaultGeometry = ((Geometry)value).g
        } else {
            f.setAttribute(name, value)
        }
        if (layer) {
            layer.queueModified(this, name)
        }
    }

    /**
     * Set a value.
     * <p><code>Field fld = feature.schema.get("name")</code></p>
     * <p><code>feature.set(fld) = "lighthouse"</code></p>
     * @param name The Field
     * @param value The new attribute value
     */
    void set(Field field, Object value) {
        set(field.name, value)
    }

    /**
     * Another way of setting a value. This method supports
     * the following syntax:
     * <p><code>feature["name"] = "lighthouse"</code></p>
     * @param name The Field name
     * @param value The new value
     */
    void putAt(String name, Object value) {
        set(name, value)
    }

    /**
     * Set a value
     * <p><code>Field fld = feature.schema.get("name")</code></p>
     * <p><code>feature[fld] = "lighthouse"</code></p>
     * @param name The Field
     * @param value The new attribute value
     */
    void putAt(Field field, Object value) {
        set(field, value)
    }

    /**
     * Set the values of this Feature based on values from another Feature
     * @param f The other Feature whose values we want
     */
    void set(Feature f) {
        f.attributes.each {k,v ->
            this.set(k,v)
        }
    }

    /**
     * Set the values of this Feature based on values from a Map
     * @param attributes A Map of values
     */
    void set(Map attributes) {
        attributes.each {k,v ->
            this.set(k,v)
        }
    }

    /**
     * Get a Map of all attributes
     * @return A Map of all attributes
     */
    Map getAttributes() {
        Map atts = [:]
        schema.fields.each{fld ->
            String name = fld.name
            Object value = f.getAttribute(name)
            atts[name] = (value instanceof JtsGeometry) ? Geometry.wrap((JtsGeometry) value) : value
        }
        atts
    }

    /**
     * The string representation
     * @return The string representation
     */
    String toString() {
        String atts = schema.fields.collect{fld -> "${fld.name}: ${get(fld.name)}"}.join(", ")
        String id = (id.startsWith(schema.name)) ? id : "${schema.name}.${id}"
        "${id} ${atts}"
    }

    /**
     * Build a SimpleFeature using the Map of data, the ID, and the Schema
     */
    private static SimpleFeature buildFeature(Map attributes, String id, Schema schema) {
        SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(schema.featureType)
        attributes.each{
            String name = it.key
            Object value = it.value
            // Shapefiles can only have field names of 10 characters
            // or less, so if the schema doesn't contain a Field by the
            // original name, try truncating it to 10 characters
            if (!schema.has(name) && name.length() >= 10) {
                name = name.substring(0,10)
            }
            if (schema.has(name)) {
                if (value instanceof Geometry) {
                    featureBuilder.set(name, value.g)
                }
                else {
                    featureBuilder.set(name, value);
                }
            }
            // Shapefiles always have a geometry field named the_geom,
            // so always set the Geometry using the schema's geom field name
            else if (value instanceof Geometry) {
                featureBuilder.set(schema.geom.name, value.g)
            }
        }
        featureBuilder.buildFeature(id)
    }

    /**
     * Build a SimpleFeature using the List of data, the ID, and the Schema
     */
    private static SimpleFeature buildFeature(List attributes, String id, Schema schema) {
        Map map = [:]
        schema.fields.eachWithIndex{fld, i ->
            map.put(fld.name, attributes[i])
        }
        buildFeature(map, id, schema)
    }

    /**
     * Build a SimpleFeature using the Map of data and the ID.  The Schema is inferred
     * from the values of the Map
     */
    private static SimpleFeature buildFeature(Map attributes, String id) {
        List<Field> fields = attributes.collect{at ->
            String name = at.key.toString()
            Object value = at.value
            String type = Schema.lookUpBinding(value.class.name)
            new Field(name, type)
        }
        Schema schema = new Schema("feature", fields)
        SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(schema.featureType)
        attributes.each{at ->
            String name = at.key.toString()
            Object value = (at.value instanceof Geometry) ? ((Geometry)at.value).g : at.value
            featureBuilder.set(name, value)
        }
        featureBuilder.buildFeature(id)
    }

}


<|start_filename|>src/test/groovy/geoscript/render/PdfTest.groovy<|end_filename|>
package geoscript.render

import geoscript.layer.Layer
import geoscript.layer.Shapefile
import geoscript.style.Fill
import geoscript.style.Stroke
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.assertNotNull

/**
 * The Pdf UnitTest
 * @author <NAME>
 */
class PdfTest {

    @TempDir
    File folder

    @Test void renderToDocument() {
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Layer layer = new Shapefile(shpFile)
        layer.style = new Stroke('black', 0.1) + new Fill('gray', 0.75)
        Map map = new Map(layers: [layer])
        Pdf pdf = new Pdf()
        def doc = pdf.render(map)
        assertNotNull(doc)
    }

    @Test void renderToOutputStream() {
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Layer layer = new Shapefile(shpFile)
        layer.style = new Stroke('black', 0.1) + new Fill('gray', 0.75)
        Map map = new Map(layers: [layer])
        Pdf pdf = new Pdf()
        File file = new File(folder,"pdf.pdf")
        OutputStream out = new FileOutputStream(file)
        pdf.render(map, out)
        out.close()
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/io/PyramidWritersTest.groovy<|end_filename|>
package geoscript.layer.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The PyramidWriters Unit Test.
 * @author <NAME>
 */
class PyramidWritersTest {

    @Test void list() {
        List<PyramidWriter> writers = PyramidWriters.list()
        assertNotNull writers
        assertTrue writers.size() > 0
    }

    @Test void find() {
        PyramidWriter writer = PyramidWriters.find("csv")
        assertNotNull writer

        writer = PyramidWriters.find("asdf")
        assertNull writer
    }
}


<|start_filename|>src/test/groovy/geoscript/geom/io/Gml3WriterTest.groovy<|end_filename|>
package geoscript.geom.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import geoscript.geom.*

/**
 * The Gml3Writer Unit Test
 * @author <NAME>
 */
class Gml3WriterTest {

    @Test void writePoint() {
        Gml3Writer writer = new Gml3Writer()
        Point p = new Point(111,-47)
        assertEquals "<gml:Point><gml:pos>111.0 -47.0</gml:pos></gml:Point>", writer.write(p)
    }

    @Test void writeLineString() {
        Gml3Writer writer = new Gml3Writer()
        LineString l = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        assertEquals "<gml:LineString><gml:posList>111.0 -47.0 123.0 -48.0 110.0 -47.0</gml:posList></gml:LineString>", writer.write(l)
    }

    @Test void writeLinearRing() {
        Gml3Writer writer = new Gml3Writer()
        LinearRing l = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        assertEquals "<gml:LinearRing><gml:posList>111.0 -47.0 123.0 -48.0 110.0 -47.0 111.0 -47.0</gml:posList></gml:LinearRing>", writer.write(l)
    }

    @Test void writePolygon() {
        Gml3Writer writer = new Gml3Writer()
        Polygon p = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        String expected = "<gml:Polygon><gml:exterior><gml:LinearRing><gml:posList>1.0 1.0 10.0 1.0 10.0 10.0 1.0 10.0 1.0 1.0</gml:posList></gml:LinearRing></gml:exterior><gml:interior><gml:LinearRing><gml:posList>2.0 2.0 4.0 2.0 4.0 4.0 2.0 4.0 2.0 2.0</gml:posList></gml:LinearRing></gml:interior><gml:interior><gml:LinearRing><gml:posList>5.0 5.0 6.0 5.0 6.0 6.0 5.0 6.0 5.0 5.0</gml:posList></gml:LinearRing></gml:interior></gml:Polygon>"
        String actual = writer.write(p)
        assertEquals expected, writer.write(p)
    }

    @Test void writeMultiPoint() {
        Gml3Writer writer = new Gml3Writer()
        MultiPoint p = new MultiPoint([111,-47],[110,-46.5])
        String expected = "<gml:MultiPoint><gml:pointMember><gml:Point><gml:pos>111.0 -47.0</gml:pos></gml:Point></gml:pointMember><gml:pointMember><gml:Point><gml:pos>110.0 -46.5</gml:pos></gml:Point></gml:pointMember></gml:MultiPoint>"
        String actual = writer.write(p)
        assertEquals expected, actual
    }

    @Test void writeMultiLineString() {
        Gml3Writer writer = new Gml3Writer()
        MultiLineString m = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        String expected = """<gml:Curve><gml:segments><gml:LineStringSegment interpolation="linear"><gml:posList>1.0 2.0 3.0 4.0</gml:posList></gml:LineStringSegment><gml:LineStringSegment interpolation="linear"><gml:posList>5.0 6.0 7.0 8.0</gml:posList></gml:LineStringSegment></gml:segments></gml:Curve>"""
        String actual = writer.write(m)
        assertEquals expected, actual
    }

    @Test void writeMultiPolygon() {
        Gml3Writer writer = new Gml3Writer()
        MultiPolygon mp = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        String expected = "<gml:MultiSurface><gml:surfaceMember><gml:Polygon><gml:exterior><gml:LinearRing><gml:posList>1.0 2.0 3.0 4.0 5.0 6.0 1.0 2.0</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></gml:surfaceMember><gml:surfaceMember><gml:Polygon><gml:exterior><gml:LinearRing><gml:posList>7.0 8.0 9.0 10.0 11.0 12.0 7.0 8.0</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></gml:surfaceMember></gml:MultiSurface>"
        String actual = writer.write(mp)
        assertEquals expected, actual
    }

    @Test void writeGeometryCollection() {
        Gml3Writer writer = new Gml3Writer()
        GeometryCollection gc = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        String expected = "<gml:MultiGeometry><gml:geometryMember><gml:Point><gml:pos>100.0 0.0</gml:pos></gml:Point></gml:geometryMember><gml:geometryMember><gml:LineString><gml:posList>101.0 0.0 102.0 1.0</gml:posList></gml:LineString></gml:geometryMember></gml:MultiGeometry>"
        String actual = writer.write(gc)
        assertEquals expected, actual
    }

    /**
     * Compare GML against GeoTool's awesome GML support
     * @param g A GeoScript Geometry
     */
    private void print(Geometry g) {
        geoscript.layer.Layer layer = new geoscript.layer.Layer()
        layer.add(layer.schema.feature([g]))
        layer.toGML()
    }
}



<|start_filename|>src/main/groovy/geoscript/layer/Graticule.groovy<|end_filename|>
package geoscript.layer

import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.geom.Bounds
import geoscript.workspace.Memory
import geoscript.workspace.Workspace
import org.geotools.data.FeatureSource
import org.geotools.data.simple.SimpleFeatureSource
import org.geotools.grid.GridElement
import org.geotools.grid.GridFeatureBuilder
import org.geotools.grid.Grids
import org.geotools.grid.Lines
import org.geotools.grid.hexagon.HexagonOrientation
import org.geotools.grid.hexagon.Hexagons
import org.geotools.grid.oblong.Oblongs
import org.geotools.grid.ortholine.LineOrientation
import org.geotools.grid.ortholine.OrthoLineDef

/**
 * Create graticule or grid Layers.
 * <p><blockquote><pre>
 * Layer layer = Graticule.createHexagons(new Bounds(0,0,50,50), 5, 1, "angled")
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Graticule {

    /**
     * Creates a square polygon graticule.
     * @param options The optional named parameters:
     * <ul>
     *     <li> workspace = The Workspace (defaults to Memory)</li>
     *     <li> layer = The new layer name (defaults to graticule)</li>
     *     <li> schema = The Schema (defaults to Polygon geometry and integer id attribute)</li>
     *     <li> setAttributes = A Closure that takes a GridElement and Map of attributes which you can use to set
     *     custom attributes</li>
     *     <li> createFeature = A Closure that takes a GridElement and return a boolean value whether the Feature should
     *     be created or not</li>
     * </ul>
     * @param bounds The Bounds
     * @param length The square cell width and height
     * @param spacing The vertex spacing (-1 for non densified)
     * @return A Layer
     */
    static Layer createSquares(Map options = [:], Bounds bounds, double length, double spacing) {
        SimpleFeatureSource fs = Grids.createSquareGrid(bounds.env, length, spacing, createGridFeatureBuilder("Polygon", options))
        createLayer(fs, options)
    }

    /**
     * Creates a oval polygon graticule.
     * @param options The optional named parameters:
     * <ul>
     *     <li> workspace = The Workspace (defaults to Memory)</li>
     *     <li> layer = The new layer name (defaults to graticule)</li>
     *     <li> schema = The Schema (defaults to Polygon geometry and integer id attribute)</li>
     *     <li> setAttributes = A Closure that takes a GridElement and Map of attributes which you can use to set
     *     custom attributes</li>
     *     <li> createFeature = A Closure that takes a GridElement and return a boolean value whether the Feature should
     *     be created or not</li>
     * </ul>
     * @param bounds The Bounds
     * @param length The square cell width and height
     * @return A Layer
     */
    static Layer createOvals(Map options = [:], Bounds bounds, double length) {
        SimpleFeatureSource fs = Grids.createOvalGrid(bounds.env, length, createGridFeatureBuilder("Polygon", options))
        createLayer(fs, options)
    }

    /**
     * Creates a hexagon polygon graticule.
     * @param options The optional named parameters:
     * <ul>
     *     <li> workspace = The Workspace (defaults to Memory)</li>
     *     <li> layer = The new layer name (defaults to graticule)</li>
     *     <li> schema = The Schema (defaults to Polygon geometry and integer id attribute)</li>
     *     <li> setAttributes = A Closure that takes a GridElement and Map of attributes which you can use to set
     *     custom attributes</li>
     *     <li> createFeature = A Closure that takes a GridElement and return a boolean value whether the Feature should
     *     be created or not</li>
     * </ul>
     * @param bounds The Bounds
     * @param length The hexagon cell width and height
     * @param spacing The vertex spacing (-1 for non densified)
     * @param orientation The cell orientation (flat or angled)
     * @return A Layer
     */
    static Layer createHexagons(Map options = [:], Bounds bounds, double length, double spacing, String orientation) {
        HexagonOrientation hexagonOrientation = HexagonOrientation.FLAT
        if (orientation.equalsIgnoreCase("flat")) {
            hexagonOrientation = HexagonOrientation.FLAT
        } else if (orientation.equalsIgnoreCase("angled")) {
            hexagonOrientation = HexagonOrientation.ANGLED
        }
        SimpleFeatureSource fs = Hexagons.createGrid(bounds.env, length, spacing, hexagonOrientation, createGridFeatureBuilder("Polygon", options))
        createLayer(fs, options)
    }

    /**
     * Creates a rectangular polygon graticule
     * @param options The optional named parameters:
     * <ul>
     *     <li> workspace = The Workspace (defaults to Memory)</li>
     *     <li> layer = The new layer name (defaults to graticule)</li>
     *     <li> schema = The Schema (defaults to Polygon geometry and integer id attribute)</li>
     *     <li> setAttributes = A Closure that takes a GridElement and Map of attributes which you can use to set
     *     custom attributes</li>
     *     <li> createFeature = A Closure that takes a GridElement and return a boolean value whether the Feature should
     *     be created or not</li>
     * </ul>
     * @param bounds The Bounds
     * @param width The cell width
     * @param height The cell height
     * @param spacing The vertex spacing (-1 for non densified)
     * @return A Layer
     */
    static Layer createRectangles(Map options = [:], Bounds bounds, double width, double height, double spacing) {
        FeatureSource fs = Oblongs.createGrid(bounds.env, width, height, spacing, createGridFeatureBuilder("Polygon", options))
        createLayer(fs, options)
    }

    /**
     * Creates a line based graticule
     * @param options The optional named parameters:
     * <ul>
     *     <li> workspace = The Workspace (defaults to Memory)</li>
     *     <li> layer = The new layer name (defaults to graticule)</li>
     *     <li> schema = The Schema (defaults to Polygon geometry and integer id attribute)</li>
     *     <li> setAttributes = A Closure that takes a GridElement and Map of attributes which you can use to set
     *     custom attributes</li>
     *     <li> createFeature = A Closure that takes a GridElement and return a boolean value whether the Feature should
     *     be created or not</li>
     * </ul>
     * @param bounds The Bounds
     * @param lineDefs A List of line definitions.  Each line definition is a map with the following properties:
     * orientation (vertical or horizontal), level, and spacing.
     * @param spacing The vertex spacing (-1 for non densified)
     * @return A Layer
     */
    static Layer createLines(Map options = [:], Bounds bounds, List<Map> lineDefs, double spacing) {
        List lineDefinitions = lineDefs.collect { Map values ->
            String lineOrientationStr = values.get("orientation","vertical")
            LineOrientation lineOrientation = lineOrientationStr.equalsIgnoreCase("vertical") ?
                    LineOrientation.VERTICAL : LineOrientation.HORIZONTAL
            int level = values.get("level")
            int lineSpacing = values.get("spacing")
            new OrthoLineDef(lineOrientation, level, lineSpacing)
        }
        SimpleFeatureSource fs = Lines.createOrthoLines(bounds.env, lineDefinitions, spacing, createGridFeatureBuilder("LineString", options))
        createLayer(fs, options)
    }

    private static createLayer(FeatureSource fs, Map options) {
        String layerName = options.get("layer", options.schema ? options.schema.name : "graticule")
        Workspace workspace = options.get("workspace", new Memory())
        Layer layer = workspace.create(layerName, new Schema(fs.schema).fields)
        layer.add(fs.features)
        layer
    }

    private static GridFeatureBuilder createGridFeatureBuilder(String geometryType, Map options) {
        Schema schema = options.get("schema", new Schema("grid", [
                new Field("geom", geometryType),
                new Field("id", "int")
        ]))
        Closure setAttributes = options.get("setAttributes")
        Closure createFeature = options.get("createFeature")
        new GridFeatureBuilder(schema.featureType) {
            private int id = 0
            @Override
            void setAttributes(GridElement gridElement, Map<String, Object> attributes) {
                if (setAttributes) {
                    setAttributes.call(gridElement, attributes)
                } else {
                    attributes.put("id", Integer.valueOf(++this.id))
                }
            }
            @Override
            boolean getCreateFeature(GridElement el) {
                if (createFeature) {
                    createFeature.call(el)
                } else {
                    super.getCreateFeature(el)
                }
            }
        }
    }
}


<|start_filename|>src/test/groovy/geoscript/geom/io/GeoJSONReaderTest.groovy<|end_filename|>
package geoscript.geom.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import geoscript.geom.*

/**
 * The GeoJSONReader Unit Test
 * @author <NAME>
 */
class GeoJSONReaderTest {

    @Test void readPoint() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "Point", "coordinates": [111.0, -47.0] }"""
        Point expected = new Point(111,-47)
        Point actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readLineString() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "LineString", "coordinates": [[111.0, -47.0], [123.0, -48.0], [110.0, -47.0]] }"""
        LineString expected = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        LineString actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPolygonWithHoles() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "Polygon", "coordinates": [[[1.0, 1.0], [10.0, 1.0], [10.0, 10.0], [1.0, 10.0], [1.0, 1.0]], [[2.0, 2.0], [4.0, 2.0], [4.0, 4.0], [2.0, 4.0], [2.0, 2.0]], [[5.0, 5.0], [6.0, 5.0], [6.0, 6.0], [5.0, 6.0], [5.0, 5.0]]] }"""
        Polygon expected = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        Polygon actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPolygonWithOutHoles() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "Polygon", "coordinates": [[[1.0, 1.0], [10.0, 1.0], [10.0, 10.0], [1.0, 10.0], [1.0, 1.0]]] }"""
        Polygon expected = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]))
        Polygon actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPoint() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "MultiPoint", "coordinates": [[111.0, -47.0], [110.0, -46.5]] }"""
        MultiPoint expected = new MultiPoint([111,-47],[110,-46.5])
        MultiPoint actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiLineString() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "MultiLineString", "coordinates": [[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]] }"""
        MultiLineString expected = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        MultiLineString actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPolygon() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "MultiPolygon", "coordinates": [[[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [1.0, 2.0]]], [[[7.0, 8.0], [9.0, 10.0], [11.0, 12.0], [7.0, 8.0]]]] }"""
        MultiPolygon expected = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        MultiPolygon actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readGeometryCollection() {
        GeoJSONReader reader = new GeoJSONReader()
        String geoJson = """{ "type": "GeometryCollection", "geometries": [{ "type": "Point", "coordinates": [100.0, 0.0] }, { "type": "LineString", "coordinates": [[101.0, 0.0], [102.0, 1.0]] }] }"""
        GeometryCollection expected = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        GeometryCollection actual = reader.read(geoJson)
        assertEquals expected.wkt, actual.wkt
    }
}




<|start_filename|>src/test/groovy/geoscript/style/ContrastEnhancementTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The ContrastEnhancement Unit Test
 * @author <NAME>
 */
class ContrastEnhancementTest {

    @Test void constructors() {
        def c = new ContrastEnhancement("normalize")
        assertEquals "normalize", c.method
        assertNull c.gammaValue

        c = new ContrastEnhancement("histogram", 0.5)
        assertEquals "histogram", c.method
        assertEquals 0.5, c.gammaValue.value, 0.1
    }

    @Test void apply() {
        def c = new ContrastEnhancement("normalize")
        def sym = Symbolizer.styleFactory.createRasterSymbolizer()
        c.apply(sym)
        assertEquals "NORMALIZE", sym.contrastEnhancement.method.name()
        assertNull sym.contrastEnhancement.gammaValue

        c = new ContrastEnhancement("histogram", 0.5)
        sym = Symbolizer.styleFactory.createRasterSymbolizer()
        c.apply(sym)
        assertEquals "HISTOGRAM", sym.contrastEnhancement.method.name()
        assertEquals 0.5, sym.contrastEnhancement.gammaValue.value, 0.1
    }

    @Test void prepare() {
        def c = new ContrastEnhancement("normalize")
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        c.prepare(rule)
        def sym = rule.symbolizers[0]
        assertEquals "NORMALIZE", sym.contrastEnhancement.method.name()
        assertNull sym.contrastEnhancement.gammaValue

        c = new ContrastEnhancement("histogram", 0.5)
        rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createRasterSymbolizer())
        c.prepare(rule)
        sym = rule.symbolizers[0]
        assertEquals "HISTOGRAM", sym.contrastEnhancement.method.name()
        assertEquals 0.5, sym.contrastEnhancement.gammaValue.value, 0.1
    }

    @Test void string() {
        def c = new ContrastEnhancement("normalize")
        assertEquals "ContrastEnhancement(method = normalize)", c.toString()

        c = new ContrastEnhancement("histogram", 0.5)
        assertEquals "ContrastEnhancement(method = histogram, gammaValue = 0.5)", c.toString()
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/CircularRingTest.groovy<|end_filename|>
package geoscript.geom

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The CircularRing Unit Test
 * @author <NAME>
 */
class CircularRingTest {

    @Test
    void createWithPoints() {
        CircularRing cr = new CircularRing(
                new Point(2, 1),
                new Point(1, 2),
                new Point(0, 1),
                new Point(1, 0),
                new Point(2, 1)
        )
        assertTrue cr.isClosed()
        assertTrue cr.isCurved()
        assertEquals "CircularRing", cr.geometryType
        assertEquals "CIRCULARSTRING (2.0 1.0, 1.0 2.0, 0.0 1.0, 1.0 0.0, 2.0 1.0)", cr.curvedWkt
        assertTrue cr.wkt.startsWith("LINEARRING")
        List pts = cr.controlPoints
        assertEquals 5, pts.size()
        assertEquals new Point(2, 1), pts[0]
        assertEquals new Point(1, 2), pts[1]
        assertEquals new Point(0, 1), pts[2]
        assertEquals new Point(1, 0), pts[3]
        assertEquals new Point(2, 1), pts[4]
        LinearRing ring = cr.linear
        assertTrue ring.numPoints > 5
    }

    @Test
    void createWithDoubleLists() {
        CircularRing cr = new CircularRing(
                [2, 1],
                [1, 2],
                [0, 1],
                [1, 0],
                [2, 1]
        )
        assertTrue cr.isClosed()
        assertTrue cr.isCurved()
        assertEquals "CircularRing", cr.geometryType
        assertEquals "CIRCULARSTRING (2.0 1.0, 1.0 2.0, 0.0 1.0, 1.0 0.0, 2.0 1.0)", cr.curvedWkt
        assertTrue cr.wkt.startsWith("LINEARRING")
        List pts = cr.controlPoints
        assertEquals 5, pts.size()
        assertEquals new Point(2, 1), pts[0]
        assertEquals new Point(1, 2), pts[1]
        assertEquals new Point(0, 1), pts[2]
        assertEquals new Point(1, 0), pts[3]
        assertEquals new Point(2, 1), pts[4]
        LinearRing ring = cr.linear
        assertTrue ring.numPoints > 5
    }

    @Test
    void createWithListOfDoubleLists() {
        CircularRing cr = new CircularRing([
                [2, 1],
                [1, 2],
                [0, 1],
                [1, 0],
                [2, 1]
        ])
        assertTrue cr.isClosed()
        assertTrue cr.isCurved()
        assertEquals "CircularRing", cr.geometryType
        assertEquals "CIRCULARSTRING (2.0 1.0, 1.0 2.0, 0.0 1.0, 1.0 0.0, 2.0 1.0)", cr.curvedWkt
        assertTrue cr.wkt.startsWith("LINEARRING")
        List pts = cr.controlPoints
        assertEquals 5, pts.size()
        assertEquals new Point(2, 1), pts[0]
        assertEquals new Point(1, 2), pts[1]
        assertEquals new Point(0, 1), pts[2]
        assertEquals new Point(1, 0), pts[3]
        assertEquals new Point(2, 1), pts[4]
        LinearRing ring = cr.linear
        assertTrue ring.numPoints > 5
    }

    @Test
    void createWithListOfPoints() {
        CircularRing cr = new CircularRing([
                new Point(2, 1),
                new Point(1, 2),
                new Point(0, 1),
                new Point(1, 0),
                new Point(2, 1)
        ])
        assertTrue cr.isClosed()
        assertTrue cr.isCurved()
        assertEquals "CircularRing", cr.geometryType
        assertEquals "CIRCULARSTRING (2.0 1.0, 1.0 2.0, 0.0 1.0, 1.0 0.0, 2.0 1.0)", cr.curvedWkt
        assertTrue cr.wkt.startsWith("LINEARRING")
        List pts = cr.controlPoints
        assertEquals 5, pts.size()
        assertEquals new Point(2, 1), pts[0]
        assertEquals new Point(1, 2), pts[1]
        assertEquals new Point(0, 1), pts[2]
        assertEquals new Point(1, 0), pts[3]
        assertEquals new Point(2, 1), pts[4]
        LinearRing ring = cr.linear
        assertTrue ring.numPoints > 5
    }
}


<|start_filename|>src/test/groovy/geoscript/carto/PngCartoFactoryTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue

class PngCartoFactoryTest {

    @Test
    void getName() {
        assertEquals("png", new PngCartoFactory().getName())
    }

    @Test
    void getMimeType() {
        assertEquals("image/png", new PngCartoFactory().getMimeType())
    }

    @Test
    void create() {
        PngCartoFactory cartoFactory = new PngCartoFactory()
        CartoBuilder cartoBuilder = cartoFactory.create(PageSize.LETTER_LANDSCAPE)
        assertTrue(cartoBuilder instanceof ImageCartoBuilder)
    }
}


<|start_filename|>src/main/groovy/geoscript/layer/Raster.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Geometry
import geoscript.proj.Projection
import geoscript.geom.Bounds
import geoscript.geom.Point
import geoscript.style.RasterSymbolizer
import geoscript.style.Style
import it.geosolutions.jaiext.shadedrelief.ShadedReliefAlgorithm
import org.geotools.coverage.Category
import org.geotools.coverage.GridSampleDimension
import org.geotools.coverage.grid.GridCoordinates2D
import org.geotools.coverage.grid.GridCoverage2D
import org.geotools.coverage.grid.GridEnvelope2D
import org.geotools.coverage.grid.GridGeometry2D
import org.geotools.coverage.processing.CoverageProcessor
import org.geotools.coverage.processing.OperationJAI
import org.geotools.coverage.processing.Operations
import org.geotools.geometry.DirectPosition2D
import org.geotools.map.GridCoverageLayer
import org.geotools.process.raster.AffineProcess
import org.geotools.process.raster.BandMergeProcess
import org.geotools.process.raster.BandSelectProcess
import org.geotools.process.raster.ContourProcess
import org.geotools.process.raster.ConvolveCoverageProcess
import org.geotools.process.raster.NormalizeCoverageProcess
import org.geotools.process.raster.PolygonExtractionProcess
import org.geotools.process.raster.RasterAsPointCollectionProcess
import org.geotools.process.raster.RasterZonalStatistics
import org.geotools.process.raster.MarchingSquaresVectorizer.ImageLoadingType
import org.geotools.process.raster.StyleCoverage
import geoscript.workspace.Memory
import geoscript.feature.Schema
import org.geotools.util.Converters
import org.geotools.util.NumberRange
import org.jaitools.imageutils.iterator.AbstractSimpleIterator
import org.jaitools.imageutils.iterator.SimpleIterator
import org.jaitools.imageutils.iterator.WindowIterator
import org.jaitools.numeric.Range
import org.geotools.coverage.grid.GridCoverageFactory
import org.opengis.coverage.SampleDimension
import org.opengis.coverage.grid.GridCoverage

import javax.media.jai.Interpolation
import javax.media.jai.RasterFactory
import javax.media.jai.TiledImage
import javax.media.jai.iterator.RandomIterFactory
import javax.media.jai.iterator.WritableRandomIter
import java.awt.Color
import java.awt.Dimension
import java.awt.Rectangle
import java.awt.image.BufferedImage
import java.awt.image.ColorModel
import java.awt.image.DataBuffer
import java.awt.image.RenderedImage
import java.awt.image.WritableRaster
import java.text.NumberFormat

/**
 * A Raster
 * @author <NAME>
 */
class Raster implements Renderable {

    /**
     * A GeoScript Raster wraps a GeoTools GridCoverage2D
     */
    GridCoverage2D coverage

    /**
     * The Style
     */
    Style style

    /**
     * The Format
     */
    Format format

    /**
     * Enable write support, but only on demand
     */
    private WritableRandomIter writable

    /**
     * Create a new Raster from a List of List of float values
     * @param data A List of Lists of float values
     * @param bounds The geographic Bounds
     */
    Raster(List data, Bounds bounds) {
        double min = Double.MAX_VALUE
        double max = Double.MIN_VALUE
        def matrix = data.collect{datum ->
            datum.collect{
                float v = it
                if (!Float.isNaN(v) && v < min) min = v
                if (!Float.isNaN(v) && v > max) max = v
                v
            } as float[]
        } as float[][]
        int width = matrix[0].length
        int height = matrix.length
        WritableRaster raster = RasterFactory.createBandedRaster(DataBuffer.TYPE_FLOAT, width, height, 1, null);
        (0..<width).each { int w ->
            (0..<height).each { int h ->
                raster.setSample(w, h, 0, matrix[h][w])
            }
        }
        def factory = new GridCoverageFactory()
        Category category = new Category("Raster", Color.BLACK, NumberRange.create(min, max))
        GridSampleDimension gridSampleDimension = new GridSampleDimension("Raster", [category] as Category[], null)
        this.coverage = factory.create("Raster", raster, bounds.env, [gridSampleDimension] as GridSampleDimension[])
        this.style = new RasterSymbolizer()
    }

    /**
     * Create a Raster from a GeoTools GridCoverage2D.
     * @param coverage The GeoTools GridCoverage2D
     * @param format The Format
     */
    Raster(GridCoverage2D coverage, Format format = null) {
       this.coverage = coverage
       this.style = new RasterSymbolizer()
       this.format = format
    }

    /**
     * Create a Raster from an Image and Bounds
     * @param image The image
     * @param bounds The Bounds
     */
    Raster(RenderedImage image, Bounds bounds) {
        GridCoverageFactory gridCoverageFactory = new GridCoverageFactory()
        this.coverage = gridCoverageFactory.create("Raster", image, bounds.env)
        this.style = new RasterSymbolizer()
    }

    /**
     * Create a new Raster form a Bounds, image size, and List of Bands
     * @param bounds A Bounds with a Projection
     * @param width The Raster width
     * @param height The Raster height
     * @param bands A List of Bands
     */
    Raster(Bounds bounds, int width, int height, List<Band> bands) {
        GridCoverageFactory gridCoverageFactory = new GridCoverageFactory()
        BufferedImage image
        if(bands.size() == 0) {
            image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY)
        } else {
            GridSampleDimension dim = bands[0].dim as GridSampleDimension
            ColorModel colorModel = bands.size() == 1 ? dim.colorModel : dim.getColorModel(0, bands.size())
            image = new BufferedImage(colorModel, colorModel.createCompatibleWritableRaster(width, height), false, null)
        }
        this.coverage = gridCoverageFactory.create(
                null,
                image,
                bounds.env,
                bands.collect { it.dim  } as GridSampleDimension[],
                null,
                null
        )
        this.style = new RasterSymbolizer()
    }

    @Override
    List getMapLayers(Bounds bounds, List size) {
        List rasterSize = this.size
        // Resample large rasters
        if (rasterSize[0] > 3000 || rasterSize[1] > 3000) {
            Raster resampledRaster = this.resample(bbox: bounds, size: size)
            [new GridCoverageLayer(resampledRaster.coverage, this.style.gtStyle)]
        }
        // Leave small rasters alone
        else {
            [new GridCoverageLayer(this.coverage, this.style.gtStyle)]
        }
    }

    /**
     * Get the Raster's name
     * @return The name
     */
    String getName() {
        this.coverage.name.toString()
    }

    /**
     * Get the Projection
     * @return The Projection
     */
    Projection getProj() {
        def crs = coverage.coordinateReferenceSystem
        if (crs) {
            new Projection(crs)
        } else {
            null
        }
    }

    /**
     * Set the Projection of this Raster (reprojecting if necessary)
     * @param projection The Projection
     */
    void setProj(def projection) {
        Projection p = new Projection(projection)
        if (!p.equals(getProj())) {
            this.coverage = reproject(p).coverage
        }
    }

    /**
     * Get the Bounds
     * @return The Bounds
     */
    Bounds getBounds() {
        def env = coverage.envelope
        def crs = env.coordinateReferenceSystem
        if (crs == null) {
            crs = getProj()
        }
        def l = env.lowerCorner.coordinate
        def u = env.upperCorner.coordinate
        new Bounds(l[0], l[1], u[0], u[1], new Projection(crs))
    }

    /**
     * Get the size [w,h] or [columns,rows]
     * @return The size [w,h] or [columns,rows]
     */
    List getSize() {
        def grid = coverage.gridGeometry.gridRange2D
        [grid.width as int, grid.height as int]
    }

    /**
     * Get the number of columns
     * @return The number of columns
     */
    int getCols() {
        size[0]
    }

    /**
     * Get the number of rows
     * @return The number rows
     */
    int getRows() {
        size[1]
    }

    /**
     * Get the List of Bands
     * @return The List of Bands
     */
    List<Band> getBands() {
        (0..<this.coverage.numSampleDimensions).collect { int i ->
            SampleDimension d = coverage.getSampleDimension(i)
            Band.builder()
                .description(d.description?.toString())
                .type(d.sampleDimensionType?.name())
                .noDataValues(d.noDataValues as List)
                .minimum(getMinValue(i))
                .maximum(getMaxValue(i))
                .scale(d.scale)
                .offset(d.offset)
                .unit(d.units)
                .build()
        }
    }

    /**
     * Get the minimum value for a band.  It first tries to get the value from the Band's metadata
     * and then calculates it from the actual data.
     * @param band The Band index
     * @return A minimum value
     */
    double getMinValue(int band) {
        double minValue = this.coverage.getSampleDimension(band).minimumValue
        if (Double.isInfinite(minValue)) {
            minValue = this.getExtrema().min[band]
        }
        minValue
    }

    /**
     * Get the maximum value for a band.  It first tries to get the value from the Band's metadata
     * and then calculates it from the actual data.
     * @param band The Band index
     * @return A maximum value
     */
    double getMaxValue(int band) {
        double maxValue = this.coverage.getSampleDimension(band).maximumValue
        if (Double.isInfinite(maxValue)) {
            maxValue = this.getExtrema().max[band]
        }
        maxValue
    }

    /**
     * Get the block size
     * @return The block size [w,h]
     */
    List getBlockSize() {
        def (int w, int h) = coverage.optimalDataBlockSizes
        [w,h]
    }

    /**
     * Get the pixel size
     * @return The pixel size [w,h]
     */
    List getPixelSize() {
        def b = getBounds()
        def s = getSize()
        [b.width / s[0], b.height / s[1]]
    }

    /**
     * Render the Raster in a simple GUI
     */
    void render() {
        coverage.show()
    }

    /**
     * Dispose of this Raster
     */
    void dispose() {
        coverage.dispose(false)
    }

    /**
     * Get the underlying java.awt.image.Raster
     * @return The java.awt.image.Raster
     */
    java.awt.image.Raster getData() {
        coverage.renderedImage.data
    }

    /**
     * Get the RenderedImage
     * @return The RenderedImage
     */
    RenderedImage getImage() {
        coverage.renderedImage
    }

    /**
     * Get a BufferedImage
     * @return A BufferedImage
     */
    BufferedImage getBufferedImage() {
        RenderedImage image = getImage()
        ColorModel colorModel = image.colorModel
        WritableRaster writableRaster = colorModel.createCompatibleWritableRaster(image.width, image.height)
        boolean isAlphaPremultiplied = colorModel.isAlphaPremultiplied()
        Hashtable properties = new Hashtable()
        BufferedImage result = new BufferedImage(colorModel, writableRaster, isAlphaPremultiplied, properties)
        image.copyData(writableRaster)
        result
    }

    /**
     * Get the value of the Raster at the given geographic Location.
     * If the Raster contains multiple bands a Collection of values, one for
     * each band, will be returned.
     * @param point The Point where we want a value from the Raster
     * @param type The type of value to return (double, float, int, byte, boolean, default)
     * @return A value
     */
    List eval(Point point, String type = "double") {
        def dp = new DirectPosition2D(coverage.coordinateReferenceSystem, point.x, point.y)
        if (type.equalsIgnoreCase("double")) {
            coverage.evaluate(dp, (double[]) null)
        } else if (type.equalsIgnoreCase("float")) {
            coverage.evaluate(dp, (float[]) null)
        } else if (type.equalsIgnoreCase("int")) {
            coverage.evaluate(dp, (int[]) null)
        } else if (type.equalsIgnoreCase("byte")) {
            coverage.evaluate(dp, (byte[]) null)
        } else if (type.equalsIgnoreCase("boolean")) {
            coverage.evaluate(dp, (boolean[]) null)
        } else {
            coverage.evaluate(dp)
        }
    }

    /**
     * Get the value of the Raster at the given pixel Location.
     * If the Raster contains multiple bands a Collection of values, one for
     * each band, will be returned.
     * @param x The pixel x coordinate
     * @param y The pixel y coordinate
     * @param type The type of value to return (double, float, int, byte, boolean, default)
     * @return A value
     */
    List eval(int x, int y, String type = "double") {
        eval(getPoint(x,y), type)
    }

    /**
     * Get the value of the Raster at the given geographic Location or pixel
     * location.
     * If the Raster contains multiple bands a Collection of values, one for
     * each band, will be returned.
     * @param point The Point where we want a value from the Raster
     * @return A value
     */
    List getAt(def p) {
        if (p instanceof Point) {
            eval(p as Point)
        } else {
            eval(p[0] as int, p[1] as int)
        }
    }

    /**
     * Get the value at the given geographic location (Point) or pixel location ([x,y])
     * for the given band
     * @param p The Point or Pixel (list x,y)
     * @param band The band zero based
     * @param type The type of value to return (double, float, int, byte, boolean, default)
     * @return The value
     */
    def getValue(def p, int band = 0, String type = "double") {
        if (p instanceof Point) {
            eval(p as Point, type)[band]
        } else {
            eval(p[0] as int, p[1] as int, type)[band]
        }
    }

    /**
     * Get the value at the given pixel location ([x,y])
     * for the given band
     * @param x The pixel x
     * @param y The pixel y
     * @param band The band zero based
     * @param type The type of value to return (double, float, int, byte, boolean, default)
     * @return The value
     */
    def getValue(int x, int y, int band = 0, String type = "double") {
        eval(x,y,type)[band]
    }

    /**
     * Get a List of values from the Raster
     * @param x The pixel x or col to start from
     * @param y The pixel y or row to start from
     * @param w The number of columns
     * @param h The number of rows
     * @param band The band to get values from (defaults to 0)
     * @param flat Whether the List should be returned flat (true, the default) or with nested Lists (false)
     * @return A List of values
     */
    List getValues(int x, int y, int w, int h, int band = 0, boolean flat = true) {
        Band b = bands[band]
        List list = []
        if (b.type.equalsIgnoreCase("byte")) {
            byte[] array = new byte[w * h]
            list = data.getSamples(x,y,w,h,band,array)
        } else if (b.type.equalsIgnoreCase("int")) {
            int[] array = new int[w * h]
            list = data.getSamples(x,y,w,h,band,array)
        } else if (b.type.equalsIgnoreCase("float")) {
            float[] array = new float[w * h]
            list = data.getSamples(x,y,w,h,band,array)
        } else if (b.type.equalsIgnoreCase("double")) {
            double[] array = new double[w * h]
            list = data.getSamples(x,y,w,h,band,array)
        } else if (b.type.equalsIgnoreCase("short")) {
            int[] array = new int[w * h]
            list = data.getSamples(x,y,w,h,band,array)
        }
        if (!flat) {
            list = (0..<h).collect{i ->
                int from = i * w
                int to = from + w
                list.subList(from, to)
            }
        }
        list
    }

    /**
     * Get values as a String
     * @param options Optional named parameters (prettyPrint = false | true)
     * @param x The pixel x or col to start from
     * @param y The pixel y or row to start from
     * @param w The number of columns
     * @param h The number of rows
     * @param band The band to get values from (defaults to 0)
     * @return A String of values
     */
    String getValuesAsString(Map options = [:], int x, int y, int w, int h, int band = 0) {
        boolean prettyPrint = options.get('prettyPrint', false)
        int numberOfDecimals = options.get('numberOfDecimals', 2)
        String NEW_LINE = System.getProperty("line.separator")
        StringBuilder builder = new StringBuilder()
        List values = getValues(x, y, w, h, band, false)
        int maxLength = 0
        values.eachWithIndex { List row, int r ->
            row.each { float n ->
                String s = String.valueOf(n.longValue())
                maxLength = Math.max(maxLength, s.length() + numberOfDecimals + 1)
            }
        }
        NumberFormat nf = NumberFormat.getNumberInstance()
        nf.setMinimumFractionDigits(numberOfDecimals)
        nf.setMaximumFractionDigits(numberOfDecimals)
        nf.setGroupingUsed(false)

        String line = "-" * ((w * (maxLength + 3)) + 1)
        if (prettyPrint) {
            builder.append(line).append("\n")
        }
        values.eachWithIndex { List row, int r ->
            if (prettyPrint && r > 0) {
                builder.append(line).append("\n")
            }
            builder.append(row.collect { float n ->
                String.format("${prettyPrint ? '| ' : ''}%${maxLength}s", nf.format(n))
            }.join(' ')).append("${prettyPrint ? ' |' : ''}${NEW_LINE}")
        }
        if (prettyPrint) {
            builder.append(line).append(NEW_LINE)
        }
        builder.toString()
    }

    /**
     * Call the Closure for each cell in this Raster
     * @param Named parameters
     * <ul>
     *     <li>bounds = The x,y,w,h List</li>
     *     <li>band = The band to get values from</li>
     *     <li>outside = The value for cells that land outside the Raster</li>
     *     <li>order = The order imagexy or tilexy</li>
     * </ul>
     * @param closure The Closure to call which is passed the value, the pixel x, and the pixel y
     */
    void eachCell(Map options = [:], Closure closure) {
        List bounds = options.get("bounds")
        int band = options.get("band",0)
        Number outsideValue = options.get("outside",0)
        String optionName = options.get("order","imagexy")
        SimpleIterator it = new SimpleIterator(
            image,
            bounds != null ? new Rectangle(bounds[0], bounds[1], bounds[2], bounds[3]) : null,
            outsideValue,
            optionName.equalsIgnoreCase("imagexy") ? AbstractSimpleIterator.Order.IMAGE_X_Y : AbstractSimpleIterator.Order.TILE_X_Y
        )

        while(true) {
            def value = it.getSample(band)
            def pt = it.getPos()
            closure.call(value, pt.x, pt.y)
            if (!it.next()) {
                break
            }
        }
    }

    /**
     * Call the Closure for each window of values in this Raster
     * @param options Named parameters
     * <ul>
     *     <li>bounds = The x,y,w,h as a List</li>
     *     <li>window = The size of the window (w,h)</li>
     *     <li>key = The key [x,y]</li>
     *     <li>steps = The x and y steps</li>
     *     <li>outside = The value for cells that land outside the Raster</li>
     * </ul>
     * @param closure The Closure to call which is passed the values, the pixel x, and the pixel y
     */
    void eachWindow(Map options=[:], Closure closure) {
        List bounds = options.get("bounds")
        List window = options.get("window",[3,3])
        List key = options.get("key",[0,0])
        List steps = options.get("steps",[1,1])
        Number outsideValue = options.get("outside",0)
        WindowIterator it = new WindowIterator(image,
            bounds != null ? new Rectangle(bounds[0], bounds[1], bounds[2], bounds[3]) : null,
            new Dimension(window[0], window[1]),
            new java.awt.Point(key[0], key[1]),
            steps[0], steps[1],
            outsideValue
        )
        Number[][] values
        while(it.hasNext()) {
            values = it.getWindow(values)
            def pt = it.getPos()
            closure.call(values, pt.x, pt.y)
            it.next()
        }
    }

    /**
     * Determine whether this Raster contains the geographic Point
     * @param point The Point
     * @return Whether this Raster contains the geographic Point
     */
    boolean contains(Point point) {
        List pixel = getPixel(point)
        contains(pixel[0] as int, pixel[1] as int)
    }

    /**
     * Determine whether this Raster contains the pixel coordinates
     * @param x The x pixel coordinate
     * @param y The y pixel coordinate
     * @return Whether this Raster contains the pixel coordinates
     */
    boolean contains(int x, int y) {
        (x >= 0 && x < cols) && (y >= 0 && y < rows)
    }

    /**
     * Get the value of the neighboring cells: NW N NE E SE S SE W
     * @param p The Point or Pixel
     * @param band The band (defaults to 0)
     * @return A Map of neighboring cell values
     */
    Map getNeighbors(def p, int band = 0) {
        List pixel = (p instanceof Point) ? getPixel(p) : p
        int c = pixel[0]
        int r = pixel[1]
        Map values = [:]
        values.put("nw", contains(c - 1, r - 1) ? getValue(c - 1, r - 1, band) : null)
        values.put("n", contains(c, r - 1) ? getValue(c, r - 1, band) : null)
        values.put("ne", contains(c + 1, r - 1) ? getValue(c + 1, r - 1, band) : null)
        values.put("e", contains(c + 1, r) ? getValue(c + 1, r, band) : null)
        values.put("se", contains(c + 1, r + 1) ? getValue(c + 1, r + 1, band) : null)
        values.put("s", contains(c, r + 1) ? getValue(c, r + 1, band) : null)
        values.put("sw", contains(c - 1, r + 1) ? getValue(c - 1, r + 1, band) : null)
        values.put("w", contains(c - 1, r) ? getValue(c - 1, r, band) : null)
        values
    }

    /**
     * Set the value for the given Point or Pixel
     * @param p The Point or Pixel (as List of xy coordinates)
     * @param value The new value
     * @param band The band (defaults to 0)
     */
    void setValue(def p, def value, int band = 0) {
        if (!writable) {
            enableWriteSupport()
        }
        def pixel = (p instanceof Point) ? getPixel(p) : p
        writable.setSample(pixel[0] as int, pixel[1] as int, band, value)
    }

    /**
     * Set the value for the given Point or Pixel
     * @param p The Point or Pixel (as List of xy coordinates)
     * @param value The new value
     */
    void putAt(def p, def value) {
        setValue(p, value)
    }

    /**
     * Enable write support on demand.
     */
    private void enableWriteSupport() {
        writable = RandomIterFactory.createWritable(
            coverage.dataEditable ? coverage.renderedImage : new TiledImage(coverage.renderedImage, true),
            null
        )
    }

    /**
     * Get a geographic Point from pixel coordinates
     * @param x The x pixel coordinate
     * @param y The y pixel coordinate
     * @return A geographic Point
     */
    Point getPoint(int x, int y) {
        GridGeometry2D gg = coverage.gridGeometry
        DirectPosition2D dp = gg.gridToWorld(new GridCoordinates2D(x,y))
        new Point(dp.x, dp.y)
    }

    /**
     * Get pixel coordinates from the geographic Point
     * @param p The geographic Point
     * @return A List of pixel coordinates
     */
    List getPixel(Point p) {
        GridGeometry2D gg = coverage.gridGeometry
        GridCoordinates2D gc = gg.worldToGrid(new DirectPosition2D(p.x, p.y))
        [gc.x, gc.y]
    }

    /**
     * Crop this Raster
     * @param bounds The Bounds
     * @return A new Raster
     */
    Raster crop(Bounds bounds) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("CoverageCrop").parameters
        params.parameter("Source").value = coverage
        params.parameter("Envelope").value = new org.geotools.geometry.GeneralEnvelope(bounds.env)
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Crop this Raster
     * @param geometry The Geometry
     * @return A new Raster
     */
    Raster crop(Geometry geometry) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("CoverageCrop").parameters
        params.parameter("Source").value = coverage
        params.parameter("ROI").value = geometry.g
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Crop this Raster using pixel coordinates
     * @param x1 The x of the min pixel
     * @param y1 The y of the min pixel
     * @param x2 The x of the max pixel
     * @param y2 The y of the max pixel
     * @return A new Raster
     */
    Raster crop(int x1, int y1, int x2, int y2) {
        def pt1 = getPoint(x1,y1)
        def pt2 = getPoint(x2,y2)
        crop(new Bounds(pt1.x, pt1.y, pt2.x, pt2.y))
    }

    /**
     * Scale this Raster
     * @param x The scale factor along the x axis
     * @param y The scale factor along the y axis
     * @param xTrans The x translation
     * @param yTrans The y translation
     * @param interpolation The interpolation method (bicubic, bicubic2, bilinear, nearest)
     * @return A new scaled Raster
     */
    Raster scale(float x, float y, float xTrans = 0, float yTrans = 0, String interpolation = "nearest") {
        int interp
        if (interpolation.equalsIgnoreCase("bicubic")) {
            interp = Interpolation.INTERP_BICUBIC
        } else if (interpolation.equalsIgnoreCase("bicubic2")) {
            interp = Interpolation.INTERP_BICUBIC_2
        } else if (interpolation.equalsIgnoreCase("bilinear")) {
            interp = Interpolation.INTERP_BILINEAR
        } else {
            interp = Interpolation.INTERP_NEAREST
        }
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Scale").parameters
        params.parameter("Source").value = this.coverage
        params.parameter("xScale").value = x
        params.parameter("yScale").value = y
        params.parameter("xTrans").value = xTrans
        params.parameter("yTrans").value = yTrans
        params.parameter("Interpolation").value = Interpolation.getInstance(interp);
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Reproject this Raster to another Projection creating a new Raster
     * @param proj The Projection
     * @return A new Raster
     */
    Raster reproject(Projection proj) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Resample").parameters
        params.parameter("Source").value = this.coverage
        params.parameter("CoordinateReferenceSystem").value = proj.crs
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Reclassify this Raster with a List of ranges.
     * @param options Optional named parameters can include the band to reclassify and the noData value.
     * <ul>
     *      <li>band: The band (defaults to 0)</li>
     *      <li>noData: The NO DATA value (defaults to 0)</li>
     * </ul>
     * @param ranges The List of Ranges contains one or more Maps with min, max, and value keys.
     * @return A new Raster
     */
    Raster reclassify(Map options=[:], List ranges) {
        int band = options.get("band",0)
        double noData = options.get("noData",0)
        List<Range> rangeList = ranges.collect{r ->
            Range.create(r.get("min"), r.get("minIncluded", true), r.get("max"), r.get("maxIncluded", true))
        }
        List pixelValues = ranges.collect{r -> r.get("value")}
        def process = new org.geotools.process.raster.RangeLookupProcess()
        def cov = process.execute(this.coverage, band, rangeList, pixelValues as int[], noData, null)
        new Raster(cov)
    }

    /**
     * Calculate the min and max values for each band in this Raster.
     * @return A Map containing the min and max values
     */
    Map getExtrema() {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Extrema").parameters
        params.parameter("Source").value = this.coverage
        def result = processor.doOperation(params)
        [
            min: result.getProperty("minimum"),
            max: result.getProperty("maximum")
        ]
    }

    /**
     * Get a Histogram for this Raster
     * @param options Optional named parameters can include low, high, and numBins
     * <ul>
     *     <li>low: The low value</li>
     *     <li>high: The high value</li>
     *     <li>numBins: The number of bins</li>
     * </ul>
     * @return A Histogram
     */
    Histogram getHistogram(Map options = [:]) {
        def low = options.get("low", 0.0)
        def high = options.get("high", 256.0)
        def numberOfBins = options.get("numBins", 256)
        int numberOfBands = bands.size()

        def processor = new CoverageProcessor()
        def params = processor.getOperation("Histogram").parameters
        params.parameter("source").value = this.coverage
        if (!(low instanceof List)) {
            low = [low] * numberOfBands
            params.parameter("lowValue").value = low as double[]
        }
        if (!(high instanceof List)) {
            high = [high] * numberOfBands
            params.parameter("highValue").value = high as double[]
        }
        if (!(numberOfBins instanceof List)) {
            numberOfBins = [numberOfBins] * numberOfBands
            params.parameter("numBins").value = numberOfBins as int[]
        }
        def h = processor.doOperation(params)
        new Histogram(h.getProperty("histogram"))
    }

    /**
     * Resample this Raster
     * @param options Optional named parameters can include:
     * <ul>
     *     <li>bbox: A geographic Bounds</li>
     *     <li>rect: A List of 4 pixel coordinates representing a rectangle.</li>
     *     <li>size: A List resulting width and height</li>
     * </ul>
     * @return A new Raster
     */
    Raster resample(Map options = [:]) {
        // Options
        Bounds bbox = options.get("bbox")
        List rect = options.get("rect")
        List size = options.get("size")
        // Calculate bbox
        if (!bbox) {
            if (options.containsKey("rect")) {
                def (double dx, double dy) = pixelSize
                Bounds b = bounds
                bbox = new Bounds(
                    b.minX + rect[0] * dx,
                    b.minY + rect[1] * dy,
                    b.maxX + rect[2] * dx,
                    b.maxY + rect[3] * dy,
                    b.proj
                )
            } else {
                bbox = bounds
            }
        }
        // Calculate size
        if (!size) {
            if (!rect) {
                Bounds b = bounds
                int w = this.size[0] * bbox.width / b.width
                size = [w, (w * b.aspect) as int]
            } else {
                size = [rect[2], rect[3]]
            }
        }
        // Try really hard to make sure the Bound's projection is set
        if (!bbox.proj) {
            bbox.setProj(this.getProj())
        }
        // Resample
        GridGeometry2D gg = new GridGeometry2D(new GridEnvelope2D(0,0,size[0],size[1]), bbox.env)
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Resample").parameters
        params.parameter("Source").value = this.coverage
        params.parameter("CoordinateReferenceSystem").value = proj.crs
        params.parameter("GridGeometry").value = gg
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Calculate a normalized version of the current Raster
     * @return A Raster
     */
    Raster normalize() {
        NormalizeCoverageProcess p = new NormalizeCoverageProcess()
        GridCoverage2D gc = p.execute(this.coverage)
        new Raster(gc)
    }

    /**
     * Calculate a convoluted version of the current Raster
     * @param radius The kernel radius
     * @return A Raster
     */
    Raster convolve(int radius) {
        ConvolveCoverageProcess p = new ConvolveCoverageProcess()
        GridCoverage2D gc = p.execute(this.coverage, null, radius, null, null)
        new Raster(gc)
    }

    /**
     * Calculate a convoluted version of the current Raster
     * @param width The kernel width
     * @param height The height width
     * @return A Raster
     */
    Raster convolve(int width, int height) {
        ConvolveCoverageProcess p = new ConvolveCoverageProcess()
        GridCoverage2D gc = p.execute(this.coverage, null, null, width, height)
        new Raster(gc)
    }

    /**
     * Transform this Raster using an Affine Transformation.
     * @param options The named parameters
     * <ul>
     *     <li>scalex =  The scale x parameter</li>
     *     <li>scaley =  The scale y parameter</li>
     *     <li>shearx =  The shear x parameter</li>
     *     <li>sheary =  The shear y parameter</li>
     *     <li>translatex =  The translate x parameter</li>
     *     <li>translatey =  The translate y parameter</li>
     *     <li>nodata = The nodata values</li>
     *     <li>interpolation = The Interpolate algorithm (NEAREST, BILINEAR, BICUBIC2, BICUBIC)</li>
     * </ul>
     * @return A transformed Raster
     */
    Raster transform(Map options = [:]) {
        double scalex = options.get("scalex",1)
        double scaley = options.get("scaley",1)
        double shearx = options.get("shearx",0)
        double sheary = options.get("sheary",0)
        double translatex = options.get("translatex",0)
        double translatey = options.get("translatey",0)
        def nodata = options.get("nodata")
        Interpolation interpolation = Converters.convert(options.get("interpolation"), Interpolation.class)
        AffineProcess process = new AffineProcess()
        GridCoverage gridCov = process.execute(this.coverage,
                scalex, scaley,
                shearx, sheary,
                translatex, translatey,
                nodata == null ? null : nodata as double[],
                interpolation
        )
        new Raster(gridCov)
    }

    /**
     * Create a new Raster with selection of Bands
     * @param bands The indices of the selected bands
     * @param visibleBand The visible band
     * @return A new Raster
     */
    Raster selectBands(List<Integer> bands, int visibleBand = -1) {
        BandSelectProcess process = new BandSelectProcess();
        GridCoverage2D gridCov = process.execute(this.coverage, bands as int[], visibleBand == -1 ? null : visibleBand)
        new Raster(gridCov)
    }

    /**
     * Merge a List of Rasters  into one Raster
     * @param options Optional named parameters:
     * <ul>
     *     <li>roi = regional of interest (a Geometry)</li>
     *     <li>transform = how to choose the grid to world transform (FIRST, LAST, INDEX)</li>
     *     <li>index = The index value of the Raster to choose when transform = INDEX</li>
     * </ul>
     * @param rasters A List of Rasters to merge
     * @return
     */
    static Raster merge(Map options = [:], List<Raster> rasters) {
        Geometry roi = options.get("roi")
        String transform = options.get("transform", "FIRST")
        int index = options.get("index",0)
        BandMergeProcess process = new BandMergeProcess()
        GridCoverage2D gridCov = process.execute(rasters.collect { it.coverage }, roi?.g, transform, index)
        new Raster(gridCov)
    }

    /**
     * Mosaic a List of Rasters into a single Raster
     * @param options options Optional named parameters:
     * <ul>
     *    <li>bounds = The final Bounds</li>
     *    <li>size = The final size of the Raster</li>
     * </ul>
     * @param rasters The List of Rasters
     * @return A new Raster
     */
    static Raster mosaic(Map options = [:], List<Raster> rasters) {
        Bounds bounds = options.get("bounds")
        if (!bounds) {
            rasters.each { Raster r ->
                if (!bounds) {
                    bounds = r.bounds
                } else {
                    bounds.expand(r.bounds)
                }
            }
        }
        List size = options.get("size")
        if (!size) {
            int width = 0
            int height = 0
            rasters.each { Raster r ->
                width += r.size[0]
                height += r.size[1]
            }
            size = [width, height]
        }
        GridGeometry2D gg = new GridGeometry2D(new GridEnvelope2D(0, 0, size[0], size[1]), bounds.env)
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Mosaic").parameters
        params.parameter("Sources").value = rasters.collect { Raster raster -> raster.coverage}
        params.parameter("geometry").value = gg
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }



    /**
     * Create a new Raster with styling baked in.
     * @param style The Style/Symobolizer
     * @return A new Raster
     */
    Raster stylize(Style sym) {
        def style = new StyleCoverage()
        def cov = style.execute(this.coverage, sym.gtStyle)
        new Raster(cov)
    }

    /**
     * Create a new Raster with current styling baked in.
     * @return A new Raster
     */
    Raster stylize() {
        def style = new StyleCoverage()
        def cov = style.execute(this.coverage, this.style.gtStyle)
        new Raster(cov)
    }

    /**
     * Create contours
     * @param band The Raster band
     * @param intervalOrLevels The contour interval or a List of levels
     * @param simplify Whether to simplify the contours
     * @param smooth Whether to smooth the contours
     * @return A Layer
     */
    Layer contours(int band, def intervalOrLevels, boolean simplify = true, boolean smooth = true, Bounds bounds = null) {
        def levels = null
        def interval = null
        if (intervalOrLevels instanceof Collection) {
            levels = intervalOrLevels as double[]
        } else {
            interval = intervalOrLevels as double
        }
        def fc = ContourProcess.process(coverage, band, levels, interval, simplify, smooth, bounds?.geometry?.g, null)
        Schema s = new Schema(fc.schema)
        Schema schema =  new Schema("contours", s.fields)
        Layer layer = new Memory().create(schema)
        layer.add(fc)
        layer
    }

    /**
     * Calculate the zonal statistics of this Raster
     * @param band The band
     * @param zones A Layer of polygons representing the zones
     * @param classification An optional Raster whose values are used as classes
     * @return A Layer with statistics (count, min, max, sum, avg, stddev, and optionally classification)
     */
    Layer zonalStatistics(int band, Layer zones, Raster classification = null) {
        def calculator = new RasterZonalStatistics()
        def fc = calculator.execute(this.coverage, band, zones.fs.features, classification?.coverage)
        new Layer("${zones.name}ZonalStatistics", fc)
    }

    /**
     * Convert this Raster to a Layer of polygons.
     * @param options Optional named parameters may include:
     * <ul>
     *      <li>band: The band defaults to 0</li>
     *      <li>insideEdges: Whether to include the inside edges or not. Defaults to true</li>
     *      <li>roi: The Geometry region of interest.  Defaults to null.</li>
     *      <li>noData: The List of no data values.  Defaults to null.</li>
     *      <li>range: A List of range Maps with min, minIncluded, max, and maxIncluded keys.  Defaults to null.</li>
     * </ul>
     * @return A Layer of polygons
     */
    Layer getPolygonLayer(Map options = [:]) {
        // Options
        int band = options.get("band",0)
        boolean insideEdges = options.get("insideEdges", true)
        Geometry roi = options.get("roi", null)
        def noData = options.get("noData", null)
        List ranges = options.get("ranges", null)
        // Prepare
        if (noData != null) {
            if (!(noData instanceof List)) {
                noData = [noData]
            }
            //noData = noData as Number[]
        }
        List rangeList = null
        if (ranges != null) {
            rangeList = ranges.collect{rng ->
                Range.create(rng.get("min"), rng.get("minIncluded", true), rng.get("max"), rng.get("maxIncluded", true))
            }
        }
        // Extract Polygons
        PolygonExtractionProcess process = new PolygonExtractionProcess()
        def fc = process.execute(this.coverage, band, insideEdges, roi?.g, noData, rangeList, null)
        Schema s = new Schema(fc.schema)
        Schema schema =  new Schema("polygons", s.fields)
        Layer layer = new Memory().create(schema)
        layer.add(fc)
        layer
    }

    /**
     * Convert this Raster into a Layer of Points
     * @param options Optional named parameters:
     * <ul>
     *      <li>proj: The target Projection</li>
     *      <li>scale: The scale</li>
     *      <li>interpolation: The Interpolation algorithm</li>
     *      <li>emisphere: Whether to add the emisphere or not</li>
     * </ul>
     * @return A Layer
     */
    Layer getPointLayer(Map options = [:]) {
        Projection p = options.get("proj", null)
        float scale = options.get("scale", 1.0f)
        Interpolation interpolation = Converters.convert(options.get("interpolation", "InterpolationNearest"), Interpolation.class)
        boolean emisphere = options.get("emisphere", false)

        def process = new RasterAsPointCollectionProcess()
        def fc = process.execute(coverage, p?.crs, scale, interpolation, emisphere)
        Schema s = new Schema(fc.schema)
        Schema schema =  new Schema("points", s.fields)
        Layer layer = new Memory().create(schema)
        layer.add(fc)
        layer
    }

    /**
     * Extract the foot print of this Raster as a vector Layer
     * @param options Optional named parameters:
     * <ul>
     *      <li>exclusionRange: A List of Maps with min and max values used to exclude values from the search.</li>
     *      <li>thresholdArea: A number used to exclude small Polygons</li>
     *      <li>computeSimplifiedFootprint: Whether to compute a simplified footprint or not</li>
     *      <li>simplifierFactor: A number used to simplify the geometry.</li>
     *      <li>removeCollinear: Whether to remove collinear coordinates.</li>
     *      <li>forceValid: Whether to force creation of valid polygons.</li>
     *      <li>loadingType: The image loading type (DEFERRED or IMMEDIATE)</li>
     * </ul>
     * @return A Layer
     */
    Layer extractFootPrint(Map options = [:]) {
        List exclusionRanges = options.get("exclusionRange", []).collect { Map range ->
            org.geotools.util.Range(Integer.class, range.min, range.max)
        }
        Double thresholdArea = options.get("thresholdArea")
        Boolean computeSimplifiedFootprint = options.get("computeSimplifiedFootprint")
        Double simplifierFactory = options.get("simplifierFactor")
        Boolean removeCollinear = options.get("removeCollinear")
        Boolean forceValid = options.get("forceValid")
        String loadingTypeStr = options.get("loadingType")
        ImageLoadingType loadType = null
        if (loadingTypeStr) {
            if (loadingTypeStr.equalsIgnoreCase("DEFERRED")) {
                loadType = ImageLoadingType.DEFERRED
            } else if (loadingTypeStr.equalsIgnoreCase("IMMEDIATE")) {
                loadType = ImageLoadingType.IMMEDIATE
            }
        }
        def process = new org.geotools.process.raster.FootprintExtractionProcess()
        def fc = process.execute(this.coverage, exclusionRanges, thresholdArea, computeSimplifiedFootprint,
                simplifierFactory, removeCollinear, forceValid, loadType, null)
        Schema s = new Schema(fc.schema)
        Schema schema =  new Schema("points", s.fields)
        Layer layer = new Memory().create(schema)
        layer.add(fc)
        layer
    }

    /**
     * Create a shaded relief Raster from the current Raster
     * @param options The optional named parameters.
     * <ol>
     *     <li>resX = The x resolution (defaults to 0.5d)</li>
     *     <li>resY = The y resolution (defaults to 0.5d)</li>
     *     <li>zetaFactor = The zeta factor (defaults to 1.0d)</li>
     *     <li>algorithm = The algorithm ZEVENBERGEN_THORNE | ZEVENBERGEN_THORNE_COMBINED | DEFAULT | COMBINED</li>
     * </oi>
     * @param scale The scale 1 or 10000
     * @param altitude The altitude in degrees 65
     * @param azimuth The azimuth in degrees 75
     * @return A new Raster
     */
    Raster createShadedRelief(Map options = [:], double scale, double altitude, double azimuth) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("ShadedRelief").parameters
        params.parameter("Source").value = this.coverage
        params.parameter("resX").value = options.get('resX', 0.5d)
        params.parameter("resY").value = options.get('resY', 0.5d)
        params.parameter("zetaFactor").value = options.get('zetaFactory', 1.0d)
        params.parameter("scale").value = scale
        params.parameter("altitude").value = altitude
        params.parameter("azimuth").value = azimuth
        params.parameter("algorithm").value = ShadedReliefAlgorithm.valueOf(options.get('algorithm', 'DEFAULT'))
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Add this Raster with another Raster
     * @param other Another a Raster
     * @return A new Raster
     */
    Raster add(Raster other) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Add").parameters
        params.parameter("Sources").value = [this.coverage, other.coverage]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Add a constant value to this Raster
     * @param value The constant number
     * @return A new Raster
     */
    Raster add(double value) {
        add([value])
    }

    /**
     * Add a List of constant values to this Raster
     * @param values The List of constant numbers
     * @return A new Raster
     */
    Raster add(List<Double> values) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("AddConst").parameters
        params.parameter("source").value = this.coverage
        params.parameter("constants").value = values as double[]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Add this Raster with another Raster
     * <p><code>def r3 = r1 + r2</code></p>
     * @param other Another a Raster
     * @return A new Raster
     */
    Raster plus(Raster other) {
        add(other)
    }

    /**
     * Add a constant value to this Raster
     * @param value The constant number
     * @return A new Raster
     */
    Raster plus(double value) {
        add(value)
    }

    /**
     * Add a List of constant values to this Raster
     * @param values The List of constant numbers
     * @return A new Raster
     */
    Raster plus(List<Double> values) {
        add(values)
    }

    /**
     * Multiple this Raster with another Raster
     * <p><code>def r3 = r1 * r2</code></p>
     * @param other Another a Raster
     * @return A new Raster
     */
    Raster multiply(Raster other) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Multiply").parameters
        params.parameter("Sources").value = [this.coverage, other.coverage]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Multiply this Raster by a List of constant values
     * @param values The list of constant values
     * @return A new Raster
     */
    Raster multiply(List<Double> values) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("MultiplyConst").parameters
        params.parameter("Source").value = this.coverage
        params.parameter("constants").value = values as double[]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Multiply this Raster by a constant value
     * @param values The constant value
     * @return A new Raster
     */
    Raster multiply(double value) {
        multiply([value])
    }

    /**
     * Divide this Raster by another Raster
     * @param other The other Raster
     * @return A new Raster
     */
    Raster div(Raster other) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Divide").parameters
        params.parameter("Source0").value = this.coverage
        params.parameter("Source1").value = other.coverage
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Divide this Raster by a constant value
     * @param value The constant value
     * @return A new Raster
     */
    Raster div(double value) {
        div([value])
    }

    /**
     * Divide this Raster by a List of constant values
     * @param values A List of constant values
     * @return A new Raster
     */
    Raster div(List<Double> values) {
        divide(values)
    }

    /**
     * Divide this Raster by another Raster
     * @param other The other Raster
     * @return A new Raster
     */
    Raster divide(Raster other) {
        div(other)
    }

    /**
     * Divide this Raster by a constant value
     * @param value The constant value
     * @return A new Raster
     */
    Raster divide(double value) {
        divide([value])
    }

    /**
     * Divide this Raster by a List of constant values
     * @param values A List of constant values
     * @return A new Raster
     */
    Raster divide(List<Double> values) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("DivideByConst").parameters
        params.parameter("Source").value = this.coverage
        params.parameter("constants").value = values as double[]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Subtract this Raster from an other Raster
     * @param other The other Raster
     * @return A new Raster
     */
    Raster minus(Raster other) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Subtract").parameters
        params.parameter("Source0").value = this.coverage
        params.parameter("Source1").value = other.coverage
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Subtract this Raster from a constant value
     * @param value The constant value
     * @return A new Raster
     */
    Raster minus(double value) {
        minus([value])
    }

    /**
     * Subtract this Raster from a List of constant values
     * @param values The List of constant values
     * @return A new Raster
     */
    Raster minus(List<Double> values) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("SubtractConst").parameters
        params.parameter("Source").value = this.coverage
        params.parameter("constants").value = values as double[]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Subtract a constant value from this Raster
     * @param value The constant value
     * @return A new Raster
     */
    Raster minusFrom(double value) {
        minusFrom([value])
    }

    /**
     * Subtract a List of constant values from this Raster
     * @param values The List of constant values
     * @return A new Raster
     */
    Raster minusFrom(List<Double> values) {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("SubtractFromConst").parameters
        params.parameter("source").value = this.coverage
        params.parameter("constants").value = values as double[]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Create a new Raster by inverting the values of this Raster
     * @return A new Raster
     */
    Raster invert() {
        def processor = new CoverageProcessor()
        def params = processor.getOperation("Invert").parameters
        params.parameter("Sources").value = [this.coverage]
        def newCoverage = processor.doOperation(params)
        new Raster(newCoverage)
    }

    /**
     * Create a new Raster by inverting the values of this Raster.
     * Overrides the - operator
     * <p><blockquote><pre>
     * def invertedRaster = -raster
     * </pre></blockquote></p>
     * @return A new Raster
     */
    Raster negative() {
        invert()
    }

    /**
     * Create a new Raster by calculating the absolute value for every cell.
     * @return A new Raster
     */
    Raster absolute() {
        def ops = new Operations()
        def cov = ops.absolute(this.coverage)
        new Raster(cov)
    }

    /**
     * Create a new Raster by calculating the exponential value for every cell.
     * @return A new Raster
     */
    Raster exp() {
        def ops = new Operations()
        def cov = ops.exp(this.coverage)
        new Raster(cov)
    }

    /**
     * Create a new Raster by calculating the log value for every cell.
     * @return A new Raster
     */
    Raster log() {
        def ops = new Operations()
        def cov = ops.log(this.coverage)
        new Raster(cov)
    }

    static class Divide extends OperationJAI {

        private static final long serialVersionUID = 3559075474256896861L

        Divide() {
            super("Divide");
        }

        @Override
        protected NumberRange deriveRange(final NumberRange[] ranges, final OperationJAI.Parameters parameters) {
            if (ranges != null && ranges.length == 2){
                final NumberRange range0 = ranges[0]
                final NumberRange range1 = ranges[1]
                final double min0 = range0.getMinimum()
                final double min1 = range1.getMinimum()
                final double max0 = range0.getMaximum()
                final double max1 = range1.getMaximum()
                final double max = max0 != 0.0 ? max0 / max1 : 0.0
                final double min = min0 != 0.0 ? min0 / min1 : 0.0
                return NumberRange.create(min, max)
            }
            return null
        }
    }

    static class Subtract extends OperationJAI {

        private static final long serialVersionUID = -4029879625681129215L

        Subtract() {
            super("Subtract");
        }

        @Override
        protected NumberRange deriveRange(final NumberRange[] ranges, final OperationJAI.Parameters parameters) {
            if (ranges != null && ranges.length == 2){
                final NumberRange range0 = ranges[0]
                final NumberRange range1 = ranges[1]
                final double min0 = range0.getMinimum()
                final double min1 = range1.getMinimum()
                final double max0 = range0.getMaximum()
                final double max1 = range1.getMaximum()
                final double max = max0 - max1
                final double min = min0 - min1
                return NumberRange.create(Math.min(min,max), Math.max(min,max))
            }
            return null
        }
    }
}


<|start_filename|>src/test/groovy/geoscript/feature/io/SchemaWritersTest.groovy<|end_filename|>
package geoscript.feature.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The SchemaWriters Unit Test
 * @author <NAME>
 */
class SchemaWritersTest {

    @Test void list() {
        List<SchemaWriter> writers = SchemaWriters.list()
        assertNotNull writers
        assertTrue writers.size() > 0
    }

    @Test void find() {
        SchemaWriter writer = SchemaWriters.find("json")
        assertNotNull writer

        writer = SchemaWriters.find("string")
        assertNotNull writer

        writer = SchemaWriters.find("xml")
        assertNotNull writer

        writer = SchemaWriters.find("asdf")
        assertNull writer
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/io/KmlReaderTest.groovy<|end_filename|>
package geoscript.geom.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import geoscript.geom.*

import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertTrue


/**
 * The KmlReader UnitTest
 * @author <NAME>
 */
class KmlReaderTest {

    @Test void readPoint() {
        KmlReader reader = new KmlReader()
        String kml = "<Point><coordinates>111.0,-47.0</coordinates></Point>"
        Point expected = new Point(111,-47)
        Point actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readLineString() {
        KmlReader reader = new KmlReader()
        String kml = "<LineString><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0</coordinates></LineString>"
        LineString expected = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        LineString actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readLinearRing() {
        KmlReader reader = new KmlReader()
        String kml = "<LinearRing><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0 111.0,-47.0</coordinates></LinearRing>"
        LinearRing expected = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        LinearRing actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPolygon() {
        KmlReader reader = new KmlReader()
        String kml = "<Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>2.0,2.0 4.0,2.0 4.0,4.0 2.0,4.0 2.0,2.0</coordinates></LinearRing></innerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>5.0,5.0 6.0,5.0 6.0,6.0 5.0,6.0 5.0,5.0</coordinates></LinearRing></innerBoundaryIs></Polygon>"
        Polygon expected = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        Polygon actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPolygonNoHoles() {
        KmlReader reader = new KmlReader()
        String kml = "<Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</coordinates></LinearRing></outerBoundaryIs></Polygon>"
        Polygon expected = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]))
        Polygon actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPoint() {
        KmlReader reader = new KmlReader()
        String kml = "<MultiGeometry><Point><coordinates>111.0,-47.0</coordinates></Point><Point><coordinates>110.0,-46.5</coordinates></Point></MultiGeometry>"
        MultiPoint expected = new MultiPoint([111,-47],[110,-46.5])
        MultiPoint actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiLineString() {
        KmlReader reader = new KmlReader()
        String kml = "<MultiGeometry><LineString><coordinates>1.0,2.0 3.0,4.0</coordinates></LineString><LineString><coordinates>5.0,6.0 7.0,8.0</coordinates></LineString></MultiGeometry>"
        MultiLineString expected = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        MultiLineString actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readMultiPolygon() {
        KmlReader reader = new KmlReader()
        String kml = "<MultiGeometry><Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,2.0 3.0,4.0 5.0,6.0 1.0,2.0</coordinates></LinearRing></outerBoundaryIs></Polygon><Polygon><outerBoundaryIs><LinearRing><coordinates>7.0,8.0 9.0,10.0 11.0,12.0 7.0,8.0</coordinates></LinearRing></outerBoundaryIs></Polygon></MultiGeometry>"
        MultiPolygon expected = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        MultiPolygon actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readGeometryCollection() {
        KmlReader reader = new KmlReader()
        String kml = "<MultiGeometry><Point><coordinates>100.0,0.0</coordinates></Point><LineString><coordinates>101.0,0.0 102.0,1.0</coordinates></LineString></MultiGeometry>"
        GeometryCollection expected = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        GeometryCollection actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readPolygonWithNamespace() {
        KmlReader reader = new KmlReader()
        String kml = '<kml:Polygon xmlns:kml="http://earth.google.com/kml/2.1"><kml:outerBoundaryIs><kml:LinearRing><kml:coordinates>0.0,0.0 2.0,0.0 2.0,2.0 0.0,2.0 0.0,0.0</kml:coordinates></kml:LinearRing></kml:outerBoundaryIs></kml:Polygon>'
        Polygon expected = new Polygon([0, 0], [2, 0], [2, 2], [0, 2], [0, 0])
        Polygon actual = reader.read(kml)
        assertEquals expected.wkt, actual.wkt
    }

    @Test void readNode() {
        KmlReader reader = new KmlReader()
        XmlParser parser = new XmlParser(false, false)
        // Point
        String str = "<Point><coordinates>111.0,-47.0</coordinates></Point>"
        Geometry geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("POINT (111 -47)", geom.wkt)
        // Point with namespace
        str = "<kml:Point><kml:coordinates>111.0,-47.0</kml:coordinates></kml:Point>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("POINT (111 -47)", geom.wkt)
        // LineString
        str = "<LineString><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0</coordinates></LineString>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("LINESTRING (111 -47, 123 -48, 110 -47)", geom.wkt)
        // LinearRing
        str = "<LinearRing><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0 111.0,-47.0</coordinates></LinearRing>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("LINEARRING (111 -47, 123 -48, 110 -47, 111 -47)", geom.wkt)
        // Polygon
        str = "<Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>2.0,2.0 4.0,2.0 4.0,4.0 2.0,4.0 2.0,2.0</coordinates></LinearRing></innerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>5.0,5.0 6.0,5.0 6.0,6.0 5.0,6.0 5.0,5.0</coordinates></LinearRing></innerBoundaryIs></Polygon>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("POLYGON ((1 1, 10 1, 10 10, 1 10, 1 1), (2 2, 4 2, 4 4, 2 4, 2 2), (5 5, 6 5, 6 6, 5 6, 5 5))", geom.wkt)
        // Polygon no wholes
        str = "<Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</coordinates></LinearRing></outerBoundaryIs></Polygon>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("POLYGON ((1 1, 10 1, 10 10, 1 10, 1 1))", geom.wkt)
        // Polygon with namespace
        str = '<kml:Polygon xmlns:kml="http://earth.google.com/kml/2.1"><kml:outerBoundaryIs><kml:LinearRing><kml:coordinates>0.0,0.0 2.0,0.0 2.0,2.0 0.0,2.0 0.0,0.0</kml:coordinates></kml:LinearRing></kml:outerBoundaryIs></kml:Polygon>'
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", geom.wkt)
        // MultiPoint
        str = "<MultiGeometry><Point><coordinates>111.0,-47.0</coordinates></Point><Point><coordinates>110.0,-46.5</coordinates></Point></MultiGeometry>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("MULTIPOINT ((111 -47), (110 -46.5))", geom.wkt)
        // MultiPoint with namespace
        str = "<kml:MultiGeometry><kml:Point><kml:coordinates>111.0,-47.0</kml:coordinates></kml:Point><kml:Point><kml:coordinates>110.0,-46.5</kml:coordinates></kml:Point></kml:MultiGeometry>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("MULTIPOINT ((111 -47), (110 -46.5))", geom.wkt)
        // MultiLineString
        str = "<MultiGeometry><LineString><coordinates>1.0,2.0 3.0,4.0</coordinates></LineString><LineString><coordinates>5.0,6.0 7.0,8.0</coordinates></LineString></MultiGeometry>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", geom.wkt)
        // GeometryCollection
        str = "<MultiGeometry><Point><coordinates>100.0,0.0</coordinates></Point><LineString><coordinates>101.0,0.0 102.0,1.0</coordinates></LineString></MultiGeometry>"
        geom = reader.read(parser.parse(new StringReader(str)))
        assertEquals("GEOMETRYCOLLECTION (POINT (100 0), LINESTRING (101 0, 102 1))", geom.wkt)
    }
}

<|start_filename|>src/main/groovy/geoscript/style/io/SLDReader.groovy<|end_filename|>
package geoscript.style.io

import geoscript.style.Style
import org.geotools.styling.Style as GtStyle
import org.geotools.styling.StyleFactory
import org.geotools.styling.StyleFactoryImpl
import org.geotools.xml.styling.SLDParser

/**
 * Read a Geoscript Style from a SLD File, InputStream or String
 * <p><blockquote><pre>
 * Reader reader = new SLDReader()
 * Style style = reader.read("states.sld")
 * </pre></blockquote></p>
 * @author <NAME>
 */
class SLDReader implements Reader {

    /**
     * The GeoTools StyleFactory
     */
    private static StyleFactory styleFactory = new StyleFactoryImpl()

    /**
     * Read a GeoScript Style from a SLD InputStream
     * @param input A SLD InputStream
     * @return A GeoScript Style
     */
    Style read(InputStream input) {
        SLDParser parser = new SLDParser(styleFactory, input)
        GtStyle[] styles = parser.readXML()
        new SLDStyle(styles[0])
    }

    /**
     * Read a GeoScript Style from a SLD File
     * @param file A SLD File
     * @return A GeoScript Style
     */
    Style read(File file) {
        read(new FileInputStream(file))
    }

    /**
     * Read a GeoScript Style from a SLD String
     * @param str A SLD String
     * @return A GeoScript Style
     */
    Style read(String str) {
        read(new ByteArrayInputStream(str.getBytes("UTF-8")))
    }

    /**
     * A simple GeoScript Style that wraps a GeoTools Style
     */
    private static class SLDStyle implements Style {
        private final GtStyle style
        SLDStyle(GtStyle style) {
            this.style = style
        }
        GtStyle getGtStyle() {
            style
        }
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/ParagraphItem.groovy<|end_filename|>
package geoscript.carto

import java.awt.Color
import java.awt.Font

/**
 * Add a paragraph to a cartographic document.
 * @author <NAME>
 */
class ParagraphItem extends Item {

    String text

    Font font = new Font("Arial", Font.PLAIN, 12)

    Color color = Color.BLACK

    /**
     * Create a paragraph from the top left with the given width and height.
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    ParagraphItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Set the text
     * @param text The text
     * @return The ParagraphItem
     */
    ParagraphItem text(String text) {
        this.text = text
        this
    }

    /**
     * Set the text color
     * @param color The text color
     * @return The ParagraphItem
     */
    ParagraphItem color(Color color) {
        this.color = color
        this
    }

    /**
     * Set the font
     * @param font The font
     * @return The ParagraphItem
     */
    ParagraphItem font(Font font) {
        this.font = font
        this
    }

    @Override
    String toString() {
        "ParagraphItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, text = ${text}, color = ${color}, font = ${font})"
    }

}


<|start_filename|>examples/layer_spatialite.groovy<|end_filename|>
import geoscript.layer.Layer
import geoscript.workspace.SpatiaLite

SpatiaLite spatiaLite = new SpatiaLite("naturalearth.sqlite")

println "Layers:"
spatiaLite.layers.each { Layer layer ->
    println "   ${layer.name}"
    println "      # Features = ${layer.count}"
    println "      Schema = ${layer.schema}"
}

<|start_filename|>src/test/groovy/geoscript/carto/DateTextItemTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import java.awt.Color
import java.awt.Font
import java.text.SimpleDateFormat
import static org.junit.jupiter.api.Assertions.*

class DateTextItemTest {

    @Test
    void create() {

        SimpleDateFormat dateFormat = new SimpleDateFormat("M/d/yyyy")

        DateTextItem item = new DateTextItem(10,20,300,400)
            .font(new Font("Verdana", Font.BOLD, 14))
            .color(Color.BLUE)
            .verticalAlign(VerticalAlign.MIDDLE)
            .horizontalAlign(HorizontalAlign.CENTER)
            .date(dateFormat.parse("2/25/2020"))
            .format("M/d/yyyy")

        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertEquals(item.font.name, "Verdana")
        assertEquals(item.font.style, Font.BOLD)
        assertEquals(item.font.size, 14)
        assertEquals(item.color, Color.BLUE)
        assertEquals(VerticalAlign.MIDDLE, item.verticalAlign)
        assertEquals(HorizontalAlign.CENTER, item.horizontalAlign)
        assertEquals("2/25/2020", dateFormat.format(item.date))
        assertEquals("M/d/yyyy", item.format)
        String itemString = item.toString()
        assertTrue(itemString.startsWith("DateTextItem(x = 10, y = 20, width = 300, height = 400,"))
        assertTrue(itemString.contains("date = "))
        assertTrue(itemString.contains("color = "))
        assertTrue(itemString.contains("font = "))
        assertTrue(itemString.endsWith("horizontal-align = CENTER, vertical-align = MIDDLE)"))
    }


}


<|start_filename|>src/main/groovy/geoscript/carto/SvgCartoBuilder.groovy<|end_filename|>
package geoscript.carto

import org.apache.batik.anim.dom.SVGDOMImplementation
import org.apache.batik.svggen.SVGGraphics2D
import org.w3c.dom.DOMImplementation
import org.w3c.dom.Document

/**
 * Build a cartographic document as a SVG document
 * @author <NAME>
 */
class SvgCartoBuilder implements CartoBuilder {

    private final SVGGraphics2D g

    private final Java2DCartoBuilder java2DCartoBuilder

    /**
     * Create a SvgCartoBuilder with a landscape letter page size
     */
    SvgCartoBuilder() {
        this(PageSize.LETTER_LANDSCAPE)
    }

    /**
     * Create a SvgCartoBilder with the given PageSize
     * @param pageSize The PageSize
     */
    SvgCartoBuilder(PageSize pageSize) {
        DOMImplementation impl = SVGDOMImplementation.getDOMImplementation()
        String svgNS = SVGDOMImplementation.SVG_NAMESPACE_URI
        Document doc = impl.createDocument(svgNS, "svg", null)
        this.g = new SVGGraphics2D(doc)
        this.java2DCartoBuilder = new Java2DCartoBuilder(g, pageSize)
    }

    @Override
    CartoBuilder dateText(DateTextItem dateTextItem) {
        java2DCartoBuilder.dateText(dateTextItem)
        this
    }

    @Override
    CartoBuilder image(ImageItem imageItem) {
        java2DCartoBuilder.image(imageItem)
        this
    }

    @Override
    CartoBuilder line(LineItem lineItem) {
        java2DCartoBuilder.line(lineItem)
        this
    }

    @Override
    CartoBuilder map(MapItem mapItem) {
        java2DCartoBuilder.map(mapItem)
        this
    }

    @Override
    CartoBuilder northArrow(NorthArrowItem northArrowItem) {
        java2DCartoBuilder.northArrow(northArrowItem)
        this
    }

    @Override
    CartoBuilder text(TextItem textItem) {
        java2DCartoBuilder.text(textItem)
        this
    }

    @Override
    CartoBuilder paragraph(ParagraphItem paragraphItem) {
        java2DCartoBuilder.paragraph(paragraphItem)
        this
    }

    @Override
    CartoBuilder rectangle(RectangleItem rectangleItem) {
        java2DCartoBuilder.rectangle(rectangleItem)
        this
    }

    @Override
    CartoBuilder scaleText(ScaleTextItem scaleTextItem) {
        java2DCartoBuilder.scaleText(scaleTextItem)
        this
    }

    @Override
    CartoBuilder scaleBar(ScaleBarItem scaleBarItem) {
        java2DCartoBuilder.scaleBar(scaleBarItem)
        this
    }

    @Override
    CartoBuilder grid(GridItem gridItem) {
        java2DCartoBuilder.grid(gridItem)
        this
    }

    @Override
    CartoBuilder overViewMap(OverviewMapItem overviewMapItem) {
        java2DCartoBuilder.overViewMap(overviewMapItem)
        this
    }

    @Override
    CartoBuilder table(TableItem tableItem) {
        java2DCartoBuilder.table(tableItem)
        this
    }

    @Override
    CartoBuilder legend(LegendItem legendItem) {
        java2DCartoBuilder.legend(legendItem)
        this
    }

    @Override
    void build(OutputStream outputStream) {
        g.stream(new OutputStreamWriter(outputStream))
        java2DCartoBuilder.graphics.dispose()
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/MapItemTest.groovy<|end_filename|>
package geoscript.carto

import geoscript.layer.Shapefile
import geoscript.render.Map
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue

class MapItemTest {

    @Test
    void create() {

        Map map = new Map(layers: [
            new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        ])

        MapItem item = new MapItem(10,20,300,400)
            .map(map)

        assertEquals(map, item.map)
        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertTrue(item.toString().startsWith("MapItem(x = 10, y = 20, width = 300, height = 400, map = "))
        assertTrue(item.toString().endsWith(")"))
    }


}


<|start_filename|>src/main/groovy/geoscript/geom/io/Gml3Writer.groovy<|end_filename|>
package geoscript.geom.io

import geoscript.geom.*
import org.locationtech.jts.geom.Coordinate

/**
 * Write a Geoscript {@link geoscript.geom.Geometry Geometry} to a GML String.
 * <p><blockquote><pre>
 * Gml3Writer writer = new Gml3Writer()
 * String gml = writer.write(new {@link geoscript.geom.Point Point}(111,-47))
 *
 * &lt;gml:Point&gt;&lt;gml:pos&gt;111.0,-47.0&lt;/gml:pos&gt;&lt;/gml:Point&gt;
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Gml3Writer implements Writer{

    /**
     * Write the Geometry to GML
     * @param geom The Geometry
     * @return GML
     */
    String write(Geometry geom) {

        if (geom instanceof Point) {
            return "<gml:Point><gml:pos>${geom.x} ${geom.y}</gml:pos></gml:Point>"
        }
        else if (geom instanceof LinearRing) {
            return "<gml:LinearRing><gml:posList>${getCoordinatesAsString(geom.coordinates)}</gml:posList></gml:LinearRing>"
        }
        else if (geom instanceof LineString) {
            return "<gml:LineString><gml:posList>${getCoordinatesAsString(geom.coordinates)}</gml:posList></gml:LineString>"
        }
        else if (geom instanceof Polygon) {
            return "<gml:Polygon><gml:exterior><gml:LinearRing><gml:posList>${getCoordinatesAsString(geom.exteriorRing.coordinates)}</gml:posList></gml:LinearRing></gml:exterior>${geom.interiorRings.collect{r-> "<gml:interior><gml:LinearRing><gml:posList>" + getCoordinatesAsString(r.coordinates) + "</gml:posList></gml:LinearRing></gml:interior>"}.join('')}</gml:Polygon>"
        }
        if (geom instanceof MultiPoint) {
            return "<gml:MultiPoint>${geom.geometries.collect{g->"<gml:pointMember>" + write(g) + "</gml:pointMember>"}.join('')}</gml:MultiPoint>"
        }
        else if (geom instanceof MultiLineString) {
            // <gml:MultiCurve><gml:curveMember><gml:LineString><gml:posList>1 2 3 4</gml:posList></gml:LineString></gml:curveMember><gml:curveMember><gml:LineString><gml:posList>5 6 7 8</gml:posList></gml:LineString></gml:curveMember></gml:MultiCurve>
            return """<gml:Curve><gml:segments>${geom.geometries.collect{g->
                "<gml:LineStringSegment interpolation=\"linear\"><gml:posList>${getCoordinatesAsString(g.coordinates)}</gml:posList></gml:LineStringSegment>"
            }.join('')}</gml:segments></gml:Curve>"""
        }
        else if (geom instanceof MultiPolygon) {
            return """<gml:MultiSurface>${geom.geometries.collect{g->
                "<gml:surfaceMember>${write(g)}</gml:surfaceMember>"
            }.join('')}</gml:MultiSurface>"""
        }
        else {
            //<gml:GeometryCollection><gml:geometryMember><gml:Point><gml:pos>100 0</gml:pos></gml:Point></gml:geometryMember><gml:geometryMember><gml:LineString><gml:posList>101 0 102 1</gml:posList></gml:LineString></gml:geometryMember></gml:GeometryCollection>
            return "<gml:MultiGeometry>${geom.geometries.collect{g->"<gml:geometryMember>" + write(g) + "</gml:geometryMember>"}.join('')}</gml:MultiGeometry>"
        }
    }

    /**
     * Write an Array of Coordinates to a GML String
     * @param coords The Array of Coordinates
     * @return A GML String (x1,y1 x2,y2)
     */
    private String getCoordinatesAsString(Coordinate[] coords) {
        coords.collect{c -> "${c.x} ${c.y}"}.join(" ")
    }

}



<|start_filename|>src/test/groovy/geoscript/render/DrawTest.groovy<|end_filename|>
package geoscript.render

import org.geotools.image.test.ImageAssert
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.style.Symbolizer
import geoscript.style.Stroke
import geoscript.style.Fill
import geoscript.geom.Geometry
import geoscript.geom.Point
import static geoscript.render.Draw.*
import javax.imageio.ImageIO
import geoscript.geom.GeometryCollection
import geoscript.geom.LineString
import geoscript.feature.Field
import geoscript.feature.Feature
import geoscript.feature.Schema
import geoscript.layer.Layer
import geoscript.layer.Shapefile
import geoscript.layer.Raster
import geoscript.layer.GeoTIFF

/**
 * The Draw UnitTest
 * @author <NAME>
 */
class DrawTest {

    @TempDir
    File folder

    private File getFile(String resource) {
        return new File(getClass().getClassLoader().getResource(resource).toURI())
    }

    @Test void drawGeometry() {
        Symbolizer sym = new Stroke('black', 2) + new Fill('gray',0.75)
        Geometry geom = new Point(0,0).buffer(0.2)
        File file = new File(folder,"draw_geometry.png")
        draw(geom, style: sym, bounds: geom.bounds.scale(1.1), size: [250,250], out: file, format: "png", backgroundColor: "white")
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_geometry.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void drawGeometryToOutputStream() {
        Symbolizer sym = new Stroke('black', 2) + new Fill('gray',0.75)
        Geometry geom = new Point(0,0).buffer(0.2)
        File file = new File(folder,"draw_geometry.png")
        OutputStream outputStream = new FileOutputStream(file)
        draw(geom, style: sym, bounds: geom.bounds.scale(1.1), size: [250,250], out: outputStream, format: "png", backgroundColor: "white")
        outputStream.flush()
        outputStream.close()
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_geometry.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void drawGeometries() {
        Symbolizer sym = new Stroke('navy', 0.1)
        Geometry geom = new Point(0,0)
        List geometries = (1..10).collect{geom.buffer(it)}
        File file = new File(folder,"draw_geometries.png")
        draw(geometries, style: sym, bounds: new GeometryCollection(geometries).bounds.scale(1.1), size: [250,250], out: file, format: "png")
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_geometries.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void drawFeature() {
        Schema schema  = new Schema("shapes",[new Field("geom","Polygon"), new Field("name", "String")])
        Feature feature = new Feature([new LineString([0,0],[1,1]).bounds.polygon, "square"], "0",  schema)
        Symbolizer sym = new Stroke('navy', 0.1)
        File file = new File(folder,"draw_feature.png")
        draw(feature, style: sym, bounds: feature.geom.bounds.scale(1.1), size: [250,250], out: file, format: "png")
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_feature.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void drawLayer() {
        Symbolizer sym = new Stroke('black', 0.1) + new Fill('gray', 0.75)
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Layer layer = new Shapefile(shpFile)
        layer.style = sym
        File file = new File(folder,"draw_layer.png")
        draw(layer, bounds: layer.bounds.scale(1.1), size: [250,250], out: file, format: "png")
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_layer.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void drawRaster() {
        File tifFile = new File(getClass().getClassLoader().getResource("alki.tif").toURI())
        GeoTIFF geoTIFF = new GeoTIFF(tifFile)
        Raster raster = geoTIFF.read()
        File file = new File(folder,"draw_raster.png")
        draw(raster, bounds: raster.bounds.scale(1.1), size: [250,250], out: file, format: "png")
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_raster.png")
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void drawLayerToPdf() {
        Symbolizer sym = new Stroke('black', 0.1) + new Fill('gray', 0.75)
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Layer layer = new Shapefile(shpFile)
        layer.style = sym
        File file = new File(folder,"draw_layer.pdf")
        draw(layer, bounds: layer.bounds.scale(1.1), size: [250,250], out: file, format: "pdf")
        assertTrue file.exists()
        assertTrue file.length() > 0
    }

    @Test void drawGeometryToImage() {
        Symbolizer sym = new Stroke('black', 2) + new Fill('gray',0.75)
        Geometry geom = new Point(0,0).buffer(0.2)
        File file = new File(folder,"draw_geometry.png")
        def image = drawToImage(geom, style: sym, bounds: geom.bounds.scale(1.1), size: [250,250])
        assertNotNull(image)
        ImageIO.write(image, "png", file)
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_geometry_to_image.png")
        ImageAssert.assertEquals(expectedFile, image, 100)
    }

    @Test void drawGeometriesToImage() {
        Symbolizer sym = new Stroke('navy', 0.1)
        Geometry geom = new Point(0,0)
        List geometries = (1..10).collect{geom.buffer(it)}
        File file = new File(folder,"draw_geometries.png")
        def image = drawToImage(geometries, style: sym, bounds: new GeometryCollection(geometries).bounds.scale(1.1), size: [250,250])
        assertNotNull(image)
        ImageIO.write(image, "png", file)
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_geometries_to_image.png")
        ImageAssert.assertEquals(expectedFile, image, 100)
    }

    @Test void drawFeatureToImage() {
        Schema schema  = new Schema("shapes",[new Field("geom","Polygon"), new Field("name", "String")])
        Feature feature = new Feature([new LineString([0,0],[1,1]).bounds.polygon, "square"], "0",  schema)
        Symbolizer sym = new Stroke('navy', 0.1)
        File file = new File(folder,"draw_feature.png")
        def image = drawToImage(feature, style: sym, bounds: feature.geom.bounds.scale(1.1), size: [250,250])
        assertNotNull(image)
        ImageIO.write(image, "png", file)
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_feature_to_image.png")
        ImageAssert.assertEquals(expectedFile, image, 100)
    }

    @Test void drawLayerToImage() {
        Symbolizer sym = new Stroke('black', 0.1) + new Fill('gray', 0.75)
        File shpFile = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Layer layer = new Shapefile(shpFile)
        layer.style = sym
        File file = new File(folder,"draw_layer.png")
        def image = drawToImage(layer, bounds: layer.bounds.scale(1.1), size: [250,250])
        assertNotNull(image)
        ImageIO.write(image, "png", file)
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_layer_to_image.png")
        ImageAssert.assertEquals(expectedFile, image, 100)
    }

    @Test void drawRasterToImage() {
        File tifFile = new File(getClass().getClassLoader().getResource("alki.tif").toURI())
        GeoTIFF geoTIFF = new GeoTIFF(tifFile)
        Raster raster = geoTIFF.read()
        File file = new File(folder,"draw_raster.png")
        def image = drawToImage(raster, bounds: raster.bounds.scale(1.1), size: [250,250])
        assertNotNull(image)
        ImageIO.write(image, "png", file)
        assertTrue file.exists()
        assertTrue file.length() > 0
        File expectedFile = getFile("geoscript/render/draw_raster_to_image.png")
        ImageAssert.assertEquals(expectedFile, image, 100)
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/io/CartoReaders.groovy<|end_filename|>
package geoscript.carto.io

import geoscript.render.io.MapReader

/**
 * A Utility for finding all registered CartoReaders.
 * @author <NAME>
 */
class CartoReaders {

    /**
     * Get a List of all CartoReaders
     * @return A List of CartoReaders
     */
    static List<Reader> list() {
        ServiceLoader.load(CartoReader).iterator().collect()
    }

    /**
     * Find a CartoReader by name (json, xml)
     * @param name The name (json, xml)
     * @return A CartoReader or null
     */
    static CartoReader find(String name) {
        list().find{ CartoReader reader ->
            String readerName = reader.class.simpleName
            readerName.toLowerCase().startsWith(name.toLowerCase())
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/TileLayerFactoriesTest.groovy<|end_filename|>
package geoscript.layer

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertTrue

/**
 * The TileLayerFactories Unit Test.
 * @author <NAME>
 */
class TileLayerFactoriesTest {

    @Test void list() {
        List<TileLayerFactory> factories = TileLayerFactories.list()
        assertNotNull factories
        assertTrue factories.size() > 0
    }

}


<|start_filename|>src/test/groovy/geoscript/style/io/ColorTableWriterTest.groovy<|end_filename|>
package geoscript.style.io

import geoscript.AssertUtil
import geoscript.style.ColorMap
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

/**
 * The ColorTableWriter Unit Test
 * @author <NAME>
 */
class ColorTableWriterTest {

    @TempDir
    File folder
    
    @Test void writeToString() {
        ColorTableWriter writer = new ColorTableWriter()
        ColorMap colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])
        String actual = writer.write(colorMap)
        String expected = """70 0 128 0
256 102 51 51"""
        AssertUtil.assertStringsEqual(expected, actual)
    }

    @Test void writeToFile() {
        ColorTableWriter writer = new ColorTableWriter()
        ColorMap colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])
        File file = new File(folder,"colortable.txt")
        writer.write(colorMap, file)
        String actual = file.text
        String expected = """70 0 128 0
256 102 51 51"""
        AssertUtil.assertStringsEqual(expected, actual)
    }

    @Test void writeToOutputStream() {
        ColorTableWriter writer = new ColorTableWriter()
        ColorMap colorMap = new ColorMap([[color: "#008000", quantity:70], [color:"#663333", quantity:256]])
        File file = new File(folder,"colortable.txt")
        writer.write(colorMap, new FileOutputStream(file))
        String actual = file.text
        String expected = """70 0 128 0
256 102 51 51"""
        AssertUtil.assertStringsEqual(expected, actual)
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/io/KmlWriterTest.groovy<|end_filename|>
package geoscript.geom.io

import groovy.xml.StreamingMarkupBuilder
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import geoscript.geom.*

/**
 * The KmlWriter Unit Test
 * @author <NAME>
 */
class KmlWriterTest {

    @Test void writePoint() {
        KmlWriter writer = new KmlWriter()
        Point p = new Point(111,-47)
        assertEquals "<Point><coordinates>111.0,-47.0</coordinates></Point>", writer.write(p)
    }

    @Test void writeLineString() {
        KmlWriter writer = new KmlWriter()
        LineString l = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        assertEquals "<LineString><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0</coordinates></LineString>", writer.write(l)
    }

    @Test void writeLinearRing() {
        KmlWriter writer = new KmlWriter()
        LinearRing l = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        assertEquals "<LinearRing><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0 111.0,-47.0</coordinates></LinearRing>", writer.write(l)
    }

    @Test void writePolygon() {
        KmlWriter writer = new KmlWriter()
        Polygon p = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        String expected = "<Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>2.0,2.0 4.0,2.0 4.0,4.0 2.0,4.0 2.0,2.0</coordinates></LinearRing></innerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>5.0,5.0 6.0,5.0 6.0,6.0 5.0,6.0 5.0,5.0</coordinates></LinearRing></innerBoundaryIs></Polygon>"
        String actual = writer.write(p)
        assertEquals expected, writer.write(p)
    }

    @Test void writeMultiPoint() {
        KmlWriter writer = new KmlWriter()
        MultiPoint p = new MultiPoint([111,-47],[110,-46.5])
        assertEquals "<MultiGeometry><Point><coordinates>111.0,-47.0</coordinates></Point><Point><coordinates>110.0,-46.5</coordinates></Point></MultiGeometry>", writer.write(p)
    }

    @Test void writeMultiLineString() {
        KmlWriter writer = new KmlWriter()
        MultiLineString m = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        assertEquals "<MultiGeometry><LineString><coordinates>1.0,2.0 3.0,4.0</coordinates></LineString><LineString><coordinates>5.0,6.0 7.0,8.0</coordinates></LineString></MultiGeometry>", writer.write(m)
    }

    @Test void writeMultiPolygon() {
        KmlWriter writer = new KmlWriter()
        MultiPolygon mp = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        assertEquals "<MultiGeometry><Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,2.0 3.0,4.0 5.0,6.0 1.0,2.0</coordinates></LinearRing></outerBoundaryIs></Polygon><Polygon><outerBoundaryIs><LinearRing><coordinates>7.0,8.0 9.0,10.0 11.0,12.0 7.0,8.0</coordinates></LinearRing></outerBoundaryIs></Polygon></MultiGeometry>", writer.write(mp)
    }

    @Test void writeGeometryCollection() {
        KmlWriter writer = new KmlWriter()
        GeometryCollection gc = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        String expected = "<MultiGeometry><Point><coordinates>100.0,0.0</coordinates></Point><LineString><coordinates>101.0,0.0 102.0,1.0</coordinates></LineString></MultiGeometry>"
        String actual = writer.write(gc)
        assertEquals expected, actual
    }

    @Test void writeUsingMarkupBuilder() {
        StreamingMarkupBuilder builder = new StreamingMarkupBuilder()
        KmlWriter writer = new KmlWriter()
        // Point
        def actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new Point(-71.92, 45.256)
        } as String
        String expected = "<Point xmlns:kml='http://www.opengis.net/kml/2.2'><coordinates>-71.92,45.256</coordinates></Point>"
        assertEquals expected, actual
        // Point with namespace
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new Point(-71.92, 45.256), namespace: "kml"
        } as String
        expected = "<kml:Point xmlns:kml='http://www.opengis.net/kml/2.2'><kml:coordinates>-71.92,45.256</kml:coordinates></kml:Point>"
        assertEquals expected, actual
        // LineString
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        } as String
        expected = "<LineString xmlns:kml='http://www.opengis.net/kml/2.2'><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0</coordinates></LineString>"
        assertEquals expected, actual
        // LinearRing
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        } as String
        expected = "<LinearRing xmlns:kml='http://www.opengis.net/kml/2.2'><coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0 111.0,-47.0</coordinates></LinearRing>"
        assertEquals expected, actual
        // Polygon
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
                    [
                            new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                            new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
                    ]
            )
        } as String
        expected = "<Polygon xmlns:kml='http://www.opengis.net/kml/2.2'><outerBoundaryIs><LinearRing><coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>2.0,2.0 4.0,2.0 4.0,4.0 2.0,4.0 2.0,2.0</coordinates></LinearRing></innerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>5.0,5.0 6.0,5.0 6.0,6.0 5.0,6.0 5.0,5.0</coordinates></LinearRing></innerBoundaryIs></Polygon>"
        assertEquals expected, actual
        // MultiPoint
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new MultiPoint([111,-47],[110,-46.5])
        } as String
        expected = "<MultiGeometry xmlns:kml='http://www.opengis.net/kml/2.2'><Point><coordinates>111.0,-47.0</coordinates></Point><Point><coordinates>110.0,-46.5</coordinates></Point></MultiGeometry>"
        assertEquals expected, actual
        // MultiPoint with namespace
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new MultiPoint([111,-47],[110,-46.5]), namespace: "kml"
        } as String
        expected = "<kml:MultiGeometry xmlns:kml='http://www.opengis.net/kml/2.2'><kml:Point><kml:coordinates>111.0,-47.0</kml:coordinates></kml:Point><kml:Point><kml:coordinates>110.0,-46.5</kml:coordinates></kml:Point></kml:MultiGeometry>"
        assertEquals expected, actual
        // MultiLineString
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        } as String
        expected = "<MultiGeometry xmlns:kml='http://www.opengis.net/kml/2.2'><LineString><coordinates>1.0,2.0 3.0,4.0</coordinates></LineString><LineString><coordinates>5.0,6.0 7.0,8.0</coordinates></LineString></MultiGeometry>"
        assertEquals expected, actual
        // MultiPolygon
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        } as String
        expected = "<MultiGeometry xmlns:kml='http://www.opengis.net/kml/2.2'><Polygon><outerBoundaryIs><LinearRing><coordinates>1.0,2.0 3.0,4.0 5.0,6.0 1.0,2.0</coordinates></LinearRing></outerBoundaryIs></Polygon><Polygon><outerBoundaryIs><LinearRing><coordinates>7.0,8.0 9.0,10.0 11.0,12.0 7.0,8.0</coordinates></LinearRing></outerBoundaryIs></Polygon></MultiGeometry>"
        assertEquals expected, actual
        // GeometryCollection
        actual = builder.bind { b ->
            mkp.declareNamespace([kml: "http://www.opengis.net/kml/2.2"])
            writer.write b, new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        } as String
        expected = "<MultiGeometry xmlns:kml='http://www.opengis.net/kml/2.2'><Point><coordinates>100.0,0.0</coordinates></Point><LineString><coordinates>101.0,0.0 102.0,1.0</coordinates></LineString></MultiGeometry>"
        assertEquals expected, actual
    }
}



<|start_filename|>src/test/groovy/geoscript/layer/WorldImageTest.groovy<|end_filename|>
package geoscript.layer

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The WorldImage Unit Test
 * @author <NAME>
 */
class WorldImageTest {

    @TempDir
    File folder

    @Test void readWrite() {

        GeoTIFF geotiff = new GeoTIFF(new File(getClass().getClassLoader().getResource("alki.tif").toURI()))
        Raster gtRaster = geotiff.read()

        // GeoTIFF to TIFF + WorldFile
        File worldTiffFile = new File(folder,"raster.tif")
        WorldImage worldImage = new WorldImage(worldTiffFile)
        worldImage.write(gtRaster)
        Raster tifRaster = worldImage.read()
        assertNotNull(tifRaster)

        // GeoTIFF to JPEG + WorldFile
        File worldJpegfFile = new File(folder,"raster.jpeg")
        worldImage = new WorldImage(worldJpegfFile)
        worldImage.write(gtRaster)
        Raster jpgRaster = worldImage.read()
        assertNotNull(jpgRaster)

        // GeoTIFF to PNG + WorldFile
        File worldPngfFile = new File(folder,"raster.png")
        worldImage = new WorldImage(worldPngfFile)
        worldImage.write(gtRaster)
        Raster pngRaster = worldImage.read()
        assertNotNull(pngRaster)
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/VectorTileRendererTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.layer.io.GeoJSONWriter
import geoscript.layer.io.KmlWriter
import geoscript.layer.io.MvtWriter
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertTrue

/**
 * The VectorTileRenderer Unit Test
 * @author <NAME>
 */
class VectorTileRendererTest {

    @TempDir
    private File folder

    @Test
    void renderGeoJson() {
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        List fields = [shp.schema.get("STATE_NAME")]
        File dir = new File(folder, "states")
        dir.mkdir()
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        geoscript.layer.io.Writer writer = new GeoJSONWriter()
        VectorTileRenderer renderer = new VectorTileRenderer(writer, shp, fields)
        byte[] data = renderer.render(pyramid.bounds(new Tile(5, 5, 20)))
        assertNotNull data
        assertTrue data.length > 0
        String json = new String(data)
        assertTrue json.startsWith("{\"type\":\"FeatureCollection\"")
    }

    @Test
    void renderKml() {
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        List fields = [shp.schema.get("STATE_NAME")]
        File dir = new File(folder, "states")
        dir.mkdir()
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        geoscript.layer.io.Writer writer = new KmlWriter()
        VectorTileRenderer renderer = new VectorTileRenderer(writer, shp, fields)
        byte[] data = renderer.render(pyramid.bounds(new Tile(5, 5, 20)))
        assertNotNull data
        assertTrue data.length > 0
        String kml = new String(data)
        assertTrue kml.startsWith("<kml:kml")
    }

    @Test
    void renderMvt() {
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        List fields = [shp.schema.get("STATE_NAME")]
        File dir = new File(folder, "states")
        dir.mkdir()
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        geoscript.layer.io.Writer writer = new MvtWriter()
        VectorTileRenderer renderer = new VectorTileRenderer(writer, shp, fields)
        byte[] data = renderer.render(pyramid.bounds(new Tile(5, 5, 20)))
        assertNotNull data
        assertTrue data.length > 0
    }
}


<|start_filename|>src/test/groovy/geoscript/workspace/DirectoryTest.groovy<|end_filename|>
package geoscript.workspace

import geoscript.FileUtil
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.layer.Layer
import geoscript.feature.Field
import geoscript.feature.Schema

/**
 * The Directory UnitTest
 */
class DirectoryTest {

    @TempDir
    File folder

    @Test void constructors() {

        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI()).parentFile
        assertNotNull(file)

        Directory dir = new Directory(file)
        assertNotNull(dir)
        assertEquals "Directory", dir.format
        assertEquals "Directory[${file}]".toString(), dir.toString()

        Directory dir2 = new Directory(file.absolutePath)
        assertNotNull(dir2)
        assertEquals "Directory", dir2.format
        assertEquals "Directory[${file}]".toString(), dir2.toString()

    }

    @Test void getNames() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI()).parentFile
        assertNotNull(file)
        Directory dir = new Directory(file)
        assertNotNull(dir)
        assertEquals 2, dir.names.size()
        assertEquals "[points, states]", dir.names.toString()
        assertTrue dir.names[0] instanceof String
    }

    @Test void getLayers() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI()).parentFile
        assertNotNull(file)
        Directory dir = new Directory(file)
        assertNotNull(dir)
        assertEquals 2, dir.layers.size()
        assertEquals "[points, states]", dir.layers.toString()
        assertTrue dir.layers[0] instanceof Layer
    }
    
    @Test void get() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI()).parentFile
        assertNotNull(file)
        Directory dir = new Directory(file)
        assertNotNull(dir)

        Layer layer = dir.get("states")
        assertNotNull(layer)
        assertEquals "states", layer.name

        layer = dir.get("states.shp")
        assertNotNull(layer)
        assertEquals "states", layer.name
    }

    @Test void getAt() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI()).parentFile
        assertNotNull(file)
        Directory dir = new Directory(file)
        assertNotNull(dir)

        Layer layer = dir["states"]
        assertNotNull(layer)
        assertEquals "states", layer.name
    }

    @Test void create() {
        File file = FileUtil.createDir(folder,"points")
        Directory dir = new Directory(file)
        Layer layer = dir.create("points", [new Field("geom","Point","EPSG:4326")])
        assertNotNull(layer)
        assertTrue(new File(file,"points.shp").exists())

        Layer layer2 = dir.create(new Schema("lines", [new Field("geom","Point","EPSG:4326")]))
        assertNotNull(layer2)
        assertTrue(new File(file,"lines.shp").exists())
    }

    @Test void add() {

        File file1 = new File(getClass().getClassLoader().getResource("states.shp").toURI()).parentFile
        Directory dir1 = new Directory(file1)
        Layer layer1 = dir1.get("states")

        File file2 = FileUtil.createDir(folder,"countries")
        Directory dir2 = new Directory(file2)
        Layer layer2 = dir2.add(layer1, "countries")
        assertTrue(new File(file2,"countries.shp").exists())

        dir2.add(layer1)
        assertTrue(new File(file2,"states.shp").exists())

    }

    @Test void remove() {
        File directory = FileUtil.createDir(folder,"shps")
        Workspace workspace = new Directory(directory)
        workspace.create("points",[new Field("geom","Point","EPSG:4326")])
        workspace.create("lines",[new Field("geom","LineString","EPSG:4326")])
        workspace.create("polygons",[new Field("geom","Polygon","EPSG:4326")])
        assertTrue workspace.has("points")
        assertTrue workspace.has("lines")
        assertTrue workspace.has("polygons")
        workspace.remove("points")
        workspace.remove(workspace.get("lines"))
        workspace.remove(workspace.get("polygons"))
        assertFalse workspace.has("points")
        assertFalse workspace.has("lines")
        assertFalse workspace.has("polygons")
    }

    @Test void getWorkspaceFromString() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Directory dir = Workspace.getWorkspace("type=shapefile file=${file}")
        assertNotNull dir
        assertTrue dir.names.contains("states")
        dir = Workspace.getWorkspace("type=shapefile file=${file.parentFile}")
        assertNotNull dir
        assertTrue dir.names.contains("states")
    }

    @Test void getWorkspaceFromMap() {
        File file = new File(getClass().getClassLoader().getResource("states.shp").toURI())
        Directory dir = Workspace.getWorkspace([type: 'shapefile', file:file])
        assertNotNull dir
        assertTrue dir.names.contains("states")
        dir = Workspace.getWorkspace([type: 'shapefile', file:file.parentFile])
        assertNotNull dir
        assertTrue dir.names.contains("states")
    }

    @Test void fromUrl() {
        URL url = getClass().getClassLoader().getResource("points.zip")
        File dir = FileUtil.createDir(folder,"points")
        Directory directory = Directory.fromURL(url, dir)
        assertTrue directory.names.contains("points")
    }

    @Test void zippedUrlParamMap() {
        URL url = getClass().getClassLoader().getResource("points.zip")
        File dir = FileUtil.createDir(folder,"points")
        Directory directory = Workspace.getWorkspace([type: "shapefile", url: url, dir: dir])
        assertTrue directory.names.contains("points")
    }

    @Test void zippedUrlParamString() {
        URL url = getClass().getClassLoader().getResource("points.zip")
        File dir = FileUtil.createDir(folder,"points")
        Directory directory = Workspace.getWorkspace("type=shapefile url='${url}' dir='${dir}'")
        assertTrue directory.names.contains("points")
    }
}



<|start_filename|>src/test/groovy/geoscript/carto/PageSizeTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals


class PageSizeTest {

    @Test
    void create() {
        PageSize pageSize = new PageSize(400,500)
        assertEquals(400, pageSize.width)
        assertEquals(500, pageSize.height)
        assertEquals("PageSize(400, 500)", pageSize.toString())
    }

    @Test
    void createStandard() {
        PageSize pageSize = PageSize.LETTER_LANDSCAPE
        assertEquals(792, pageSize.width)
        assertEquals(612, pageSize.height)
        assertEquals("PageSize(792, 612)", pageSize.toString())
    }

}


<|start_filename|>src/main/groovy/geoscript/layer/Shapefile.groovy<|end_filename|>
package geoscript.layer

import geoscript.GeoScript
import geoscript.workspace.Directory
import org.geotools.data.shapefile.files.ShpFileType
import org.geotools.data.shapefile.ShapefileDumper
import org.geotools.util.URLs

/**
 * A Shapefile Layer.
 * <p>You can create a Shapefile Layer by passing the .shp file:</p>
 * <p><blockquote><pre>
 * Shapefile shp = new Shapefile('states.shp')
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Shapefile extends Layer {

    /**
     * Create a Shapefile Layer from a File
     * @param file The Shapefile file (*.shp)
     */
    Shapefile(File file) {
        super(create(file.absoluteFile))
    }

    /**
     * Create a Shapefile Layer from a File
     * @param file The Shapefile file (*.shp)
     */
    Shapefile(String file) {
        this(new File(file))
    }

    /**
     * Get the Shapefile's File
     * @return The Shapefile's File
     */
    File getFile() {
        URLs.urlToFile(new URL(fs.dataStore.shpFiles.get(ShpFileType.SHP)))
    }

    /**
     * Create a zip file with all of the Shapefile's files
     * @param options The optional named parameters
     * <ul>
     *     <li> zipFile = The optional zip File which defaults to a zip file in the same directory as the Shapefile</li>
     * </ul>
     * @return The zip File
     */
    File zip(Map options = [:]) {
        File dir = this.file.absoluteFile.parentFile
        File zipFile = options.get("zipFile", new File(dir, "${this.name}.zip"))
        GeoScript.zip(["shp", "dbf", "shx", "prj", "qix", "fix"].collect { String ext ->
            new File(dir, "${this.name}.${ext}")
        }, zipFile)
    }

    /**
     * Unzip the zip file and return a Shapefile if possible
     * @param options The optional named parameters
     * <ul>
     *     <li>dir = The directory in which the zip file is unzipped</li>
     * </ul>
     * @param file The zip File or file name
     * @return A Shapefile or null
     */
    static Shapefile unzip(Map options = [:], def file) {
        File zipFile = file instanceof File ? file as File : new File(file)
        String name = options.get("name", ".shp")
        File dir = options.get("dir", new File(zipFile.absoluteFile.parentFile, zipFile.name.substring(0, zipFile.name.lastIndexOf(".zip"))))
        GeoScript.unzip(zipFile, dir)
        File shpFile = dir.listFiles().find{ File f -> f.name.endsWith(name)}
        shpFile != null ? new Shapefile(shpFile) : null
    }

    /**
     * Dump the Layer which may contain more than one Geometry type into a Directory of Shapefiles
     * @param options The optional named parameters:
     * <ul>
     *     <li> maxShapeSize = The maximum shp size </li>
     *     <li> maxDbfSize = The maximum dbf size </li>
     * </ul>
     * @param dir The File where the Shapefiles will be written
     * @param layer The Layer to turn into Shapefiles
     * @return A Directory Workspace
     */
    static Directory dump(Map options = [:], File dir, Layer layer) {
        dump(options, dir, layer.cursor)
    }

    /**
     * Dump the Cursor which may contain more than one Geometry type into a Directory of Shapefiles
     * @param options The optional named parameters:
     * <ul>
     *     <li> maxShapeSize = The maximum shp size </li>
     *     <li> maxDbfSize = The maximum dbf size </li>
     * </ul>
     * @param dir The File where the Shapefiles will be written
     * @param cursor The Cursor to turn into Shapefiles
     * @return A Directory Workspace
     */
    static Directory dump(Map options = [:], File dir, Cursor cursor) {
        ShapefileDumper dumper = new ShapefileDumper(dir)
        if (options.maxShapeSize) {
            dumper.maxShapeSize = options.maxShapeSize
        }
        if (options.maxDbfSize) {
            dumper.maxDbfSize = options.maxDbfSize
        }
        dumper.dump(cursor.col)
        new Directory(dir)
    }


    /**
     * Create a Shapefile Layer form a File
     */
    private static Layer create(File file) {
        String fileName = file.name
        String name = fileName.substring(0, fileName.lastIndexOf('.'))
        return new Layer(name, new Directory(file.parentFile))
    }
}

<|start_filename|>src/test/groovy/geoscript/carto/JpegCartoFactoryTest.groovy<|end_filename|>
package geoscript.carto

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class JpegCartoFactoryTest {

    @Test
    void getName() {
        assertEquals("jpeg", new JpegCartoFactory().getName())
    }

    @Test
    void getMimeType() {
        assertEquals("image/jpeg", new JpegCartoFactory().getMimeType())
    }

    @Test
    void create() {
        JpegCartoFactory cartoFactory = new JpegCartoFactory()
        CartoBuilder cartoBuilder = cartoFactory.create(PageSize.LETTER_LANDSCAPE)
        assertTrue(cartoBuilder instanceof ImageCartoBuilder)
    }
}


<|start_filename|>src/test/groovy/geoscript/render/io/JsonMapReaderTest.groovy<|end_filename|>
package geoscript.render.io

import geoscript.render.Map
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class JsonMapReaderTest {

    @Test void getName() {
        MapReader mapReader = new JsonMapReader()
        assertEquals("json", mapReader.name)
    }

    @Test void readWithOptions() {
        MapReader mapReader = new JsonMapReader()
        String dir = new File("src/test/resources").absolutePath
        Map map = mapReader.read("""{
            "width": 400,
            "height": 400,
            "type": "png",
            "backgroundColor": "blue",
            "proj": "EPSG:4326",
            "bounds": {
                "minX": -135.911779,
                "minY": 36.993573,
                "maxX": -96.536779,
                "maxY": 51.405899
            },
            "layers": [
                {"layertype": "layer", "file": "${dir}/states.shp"}
            ]
        }""")
        assertEquals(400, map.width)
        assertEquals(400, map.height)
        assertEquals("png", map.type)
        assertEquals("#0000ff", map.backgroundColor)
        assertEquals(1, map.layers.size())
    }

    @Test void read() {
        MapReader mapReader = new JsonMapReader()
        String dir = new File("src/test/resources").absolutePath
        Map map = mapReader.read("""{
            "layers": [
                {"layertype": "layer", "file": "${dir}/states.shp"}
            ]
        }""")
        assertEquals(600, map.width)
        assertEquals(400, map.height)
        assertEquals("png", map.type)
        assertEquals(1, map.layers.size())
    }
}


<|start_filename|>src/test/groovy/geoscript/feature/io/XmlSchemaReaderTest.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Schema
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertNull

/**
 * The XmlSchemaReader Unit Test
 * @author <NAME>
 */
class XmlSchemaReaderTest {

    @Test void read() {
        String str = """<schema>
  <name>points</name>
  <projection>EPSG:4326</projection>
  <geometry>geom</geometry>
  <fields>
    <field>
      <name>geom</name>
      <type>Point</type>
      <projection>EPSG:4326</projection>
    </field>
    <field>
      <name>name</name>
      <type>String</type>
    </field>
    <field>
      <name>price</name>
      <type>Float</type>
    </field>
  </fields>
</schema>"""
        SchemaReader reader = new XmlSchemaReader()
        Schema schema = reader.read(str)
        assertEquals "points", schema.name
        assertEquals 3, schema.fields.size()
        assertEquals "geom", schema.fields[0].name
        assertEquals "Point", schema.fields[0].typ
        assertEquals "EPSG:4326", schema.fields[0].proj.id
        assertEquals "name", schema.fields[1].name
        assertEquals "String", schema.fields[1].typ
        assertNull schema.fields[1].proj
        assertEquals "price", schema.fields[2].name
        assertEquals "Float", schema.fields[2].typ
        assertNull schema.fields[2].proj
    }

}


<|start_filename|>src/test/groovy/geoscript/geom/io/Gml2WriterTest.groovy<|end_filename|>
package geoscript.geom.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import geoscript.geom.*

/**
 * The Gml2Writer Unit Test
 * @author <NAME>
 */
class Gml2WriterTest {

    @Test void writePoint() {
        Gml2Writer writer = new Gml2Writer()
        Point p = new Point(111,-47)
        assertEquals "<gml:Point><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point>", writer.write(p)
    }

    @Test void writeLineString() {
        Gml2Writer writer = new Gml2Writer()
        LineString l = new LineString([[111.0, -47],[123.0, -48],[110.0, -47]])
        assertEquals "<gml:LineString><gml:coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0</gml:coordinates></gml:LineString>", writer.write(l)
    }

    @Test void writeLinearRing() {
        Gml2Writer writer = new Gml2Writer()
        LinearRing l = new LinearRing([[111.0, -47],[123.0, -48],[110.0, -47],[111.0, -47]])
        assertEquals "<gml:LinearRing><gml:coordinates>111.0,-47.0 123.0,-48.0 110.0,-47.0 111.0,-47.0</gml:coordinates></gml:LinearRing>", writer.write(l)
    }

    @Test void writePolygon() {
        Gml2Writer writer = new Gml2Writer()
        Polygon p = new Polygon(new LinearRing([1,1], [10,1], [10,10], [1,10], [1,1]),
            [
                new LinearRing([2,2], [4,2], [4,4], [2,4], [2,2]),
                new LinearRing([5,5], [6,5], [6,6], [5,6], [5,5])
            ]
        )
        String expected = "<gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.0,1.0 10.0,1.0 10.0,10.0 1.0,10.0 1.0,1.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>2.0,2.0 4.0,2.0 4.0,4.0 2.0,4.0 2.0,2.0</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs><gml:innerBoundaryIs><gml:LinearRing><gml:coordinates>5.0,5.0 6.0,5.0 6.0,6.0 5.0,6.0 5.0,5.0</gml:coordinates></gml:LinearRing></gml:innerBoundaryIs></gml:Polygon>"
        String actual = writer.write(p)
        assertEquals expected, writer.write(p)
    }

    @Test void writeMultiPoint() {
        Gml2Writer writer = new Gml2Writer()
        MultiPoint p = new MultiPoint([111,-47],[110,-46.5])
        String expected = "<gml:MultiPoint><gml:pointMember><gml:Point><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point></gml:pointMember><gml:pointMember><gml:Point><gml:coordinates>110.0,-46.5</gml:coordinates></gml:Point></gml:pointMember></gml:MultiPoint>"
        String actual = writer.write(p)
        assertEquals expected, actual
    }

    @Test void writeMultiLineString() {
        Gml2Writer writer = new Gml2Writer()
        MultiLineString m = new MultiLineString(new LineString([1,2],[3,4]), new LineString([5,6],[7,8]))
        String expected = """<gml:MultiLineString><gml:lineStringMember><gml:LineString><gml:coordinates>1.0,2.0 3.0,4.0</gml:coordinates></gml:LineString></gml:lineStringMember><gml:lineStringMember><gml:LineString><gml:coordinates>5.0,6.0 7.0,8.0</gml:coordinates></gml:LineString></gml:lineStringMember></gml:MultiLineString>"""
        String actual = writer.write(m)
        assertEquals expected, actual
    }

    @Test void writeMultiPolygon() {
        Gml2Writer writer = new Gml2Writer()
        MultiPolygon mp = new MultiPolygon([[[[1,2],[3,4],[5,6],[1,2]]], [[[7,8],[9,10],[11,12],[7,8]]]])
        String expected = "<gml:MultiPolygon><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1.0,2.0 3.0,4.0 5.0,6.0 1.0,2.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember><gml:polygonMember><gml:Polygon><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>7.0,8.0 9.0,10.0 11.0,12.0 7.0,8.0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></gml:polygonMember></gml:MultiPolygon>"
        String actual = writer.write(mp)
        assertEquals expected, actual
    }

    @Test void writeGeometryCollection() {
        Gml2Writer writer = new Gml2Writer()
        GeometryCollection gc = new GeometryCollection(new Point(100.0, 0.0), new LineString([101.0, 0.0], [102.0,1.0]))
        String expected = "<gml:GeometryCollection><gml:geometryMember><gml:Point><gml:coordinates>100.0,0.0</gml:coordinates></gml:Point></gml:geometryMember><gml:geometryMember><gml:LineString><gml:coordinates>101.0,0.0 102.0,1.0</gml:coordinates></gml:LineString></gml:geometryMember></gml:GeometryCollection>"
        String actual = writer.write(gc)
        assertEquals expected, actual
    }

}



<|start_filename|>src/test/groovy/geoscript/workspace/SqliteTest.groovy<|end_filename|>
package geoscript.workspace

import geoscript.feature.Field
import geoscript.geom.Bounds
import geoscript.geom.Geometry
import geoscript.geom.LineString
import geoscript.geom.Point
import geoscript.layer.Layer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

class SqliteTest {

    @TempDir
    File folder

    @Test void read() {
        File file = new File(getClass().getClassLoader().getResource("data.sqlite").toURI())
        assertNotNull(file)
        Sqlite sqlite = new Sqlite(file)
        assertEquals "Sqlite", sqlite.format
        List<String> names = sqlite.names
        assertEquals(2, names.size())
        assertTrue(names.contains("points"))
        assertTrue(names.contains("polygons"))
        sqlite.close()
    }

    @Test void writeWKB() {
        File file = new File(folder,"features.sqlite")
        assertNotNull(file)
        Sqlite sqlite = new Sqlite(file)
        try {
            assertEquals "Sqlite", sqlite.format

            // Add
            Layer l = sqlite.create('widgets',[new Field("geom", "Point"), new Field("name", "String")])
            assertNotNull(l)
            l.add([new Point(1,1), "one"])
            l.add([new Point(2,2), "two"])
            l.add([new Point(3,3), "three"])
            assertEquals 3, l.count()
            // Get
            assertTrue(sqlite.has("widgets"))
            Layer layer = sqlite.get("widgets")
            assertEquals 3, layer.count()
            // Remove
            sqlite.remove("widgets")
            // Get
            assertFalse(sqlite.has("widgets"))

        } finally {
            sqlite.close()
        }
    }

    @Test void writeWKT() {
        File file = new File(folder,"features.sqlite")
        assertNotNull(file)
        Sqlite sqlite = new Sqlite(file, "WKT")
        try {
            assertEquals "Sqlite", sqlite.format

            // Add
            Layer l = sqlite.create('widgets',[new Field("geom", "Point"), new Field("name", "String")])
            assertNotNull(l)
            l.add([new Point(1,1), "one"])
            l.add([new Point(2,2), "two"])
            l.add([new Point(3,3), "three"])
            assertEquals 3, l.count()
            // Get
            assertTrue(sqlite.has("widgets"))
            Layer layer = sqlite.get("widgets")
            assertEquals 3, layer.count()
            // Remove
            sqlite.remove("widgets")
            // Get
            assertFalse(sqlite.has("widgets"))

        } finally {
            sqlite.close()
        }
    }

    @Test void geometries() {
        File file = new File(folder,"features.sqlite")
        assertNotNull(file)
        Sqlite sqlite = new Sqlite(file)
        try {
            assertEquals "Sqlite", sqlite.format
            Bounds bounds = new Bounds(-180, -90, 180, 90, "EPSG:4326")

            // Points
            Layer points = sqlite.create('points',[new Field("geom", "Point", "EPSG:4326"), new Field("fid", "int")])
            Geometry.createRandomPoints(bounds.geometry, 100).geometries.eachWithIndex { Geometry pt, int i ->
                points.add(points.schema.feature([fid: i, geom: pt], "point.${i}"))
            }
            assertEquals(100, points.count)

            // LineString
            Layer lines = sqlite.create('lines',[new Field("geom", "LineString", "EPSG:4326"), new Field("fid", "int")])
            Geometry.createRandomPoints(bounds.geometry, 100).geometries.eachWithIndex { Geometry pt, int i ->
                LineString lineString = new LineString([pt, pt.translate(2,3)])
                lines.add(lines.schema.feature([fid: i, geom: lineString], "line${i}"))
            }
            assertEquals(100, lines.count)

            // Polygons
            Layer polygons = sqlite.create('polygons',[new Field("geom", "Polygon", "EPSG:4326"), new Field("fid", "int")])
            Geometry.createRandomPoints(bounds.geometry, 100).geometries.eachWithIndex { Geometry pt, int i ->
                polygons.add(polygons.schema.feature([fid: i, geom: pt.buffer(2)], "polygon.${i}"))
            }
            assertEquals(100, polygons.count)

        } finally {
            sqlite.close()
        }
    }

    @Test void writeAndReadworkspaceFactory() {
        // Registered
        assertNotNull(WorkspaceFactories.list().contains(Sqlite.Factory))
        // Read
        File file = new File(getClass().getClassLoader().getResource("data.sqlite").toURI())
        assertNotNull(file)
        Sqlite sqlite = Workspace.getWorkspace([dbtype: 'sqlite', database: file])
        assertNotNull(sqlite)
        sqlite = Workspace.getWorkspace("dbtype=sqlite database=${file}")
        assertNotNull(sqlite)
        sqlite = Workspace.getWorkspace("${file}")
        assertNotNull(sqlite)
        // Write
        Bounds bounds = new Bounds(-180, -90, 180, 90, "EPSG:4326")
        File newFile = new File(folder,"features.sqlite")
        sqlite = Workspace.getWorkspace("${newFile}")
        Layer points = sqlite.create('points',[new Field("geom", "Point", "EPSG:4326"), new Field("fid", "int")])
        Geometry.createRandomPoints(bounds.geometry, 100).geometries.eachWithIndex { Geometry pt, int i ->
            points.add(points.schema.feature([fid: i, geom: pt], "point.${i}"))
        }
        assertEquals(100, points.count)

        newFile = new File(folder,"features2.sqlite")
        sqlite = Workspace.getWorkspace([type: 'sqlite', database: newFile])
        points = sqlite.create('points',[new Field("geom", "Point", "EPSG:4326"), new Field("fid", "int")])
        Geometry.createRandomPoints(bounds.geometry, 100).geometries.eachWithIndex { Geometry pt, int i ->
            points.add(points.schema.feature([fid: i, geom: pt], "point.${i}"))
        }
        assertEquals(100, points.count)
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/io/CartoReadersTest.groovy<|end_filename|>
package geoscript.carto.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class CartoReadersTest {

    @Test void find() {
        assertNotNull(CartoReaders.find("xml"))
        assertNotNull(CartoReaders.find("json"))
        assertNull(CartoReaders.find("asdf"))
    }

    @Test void list() {
        List<CartoReader> mapReaders = CartoReaders.list()
        assertFalse(mapReaders.isEmpty())
    }

}


<|start_filename|>src/main/groovy/geoscript/carto/io/CartoReader.groovy<|end_filename|>
package geoscript.carto.io

import geoscript.carto.CartoBuilder

/**
 * Read a CartoBuilder from a String
 * @author <NAME>
 */
interface CartoReader {

    /**
     * Get the name of the CartoReader
     * @return The name
     */
    String getName()

    /**
     * Read a CartoBuilder from a String
     * @param str A String
     * @return A CartoBuilder
     */
    CartoBuilder read(String str)

}

<|start_filename|>src/test/groovy/geoscript/layer/io/GeoJSONWriterTest.groovy<|end_filename|>
package geoscript.layer.io

import groovy.json.JsonSlurper
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.Point
import geoscript.workspace.Memory
import geoscript.layer.Layer
import geoscript.feature.Schema
import geoscript.feature.Field
import geoscript.feature.Feature

/**
 * The GeoJSONWriter UnitTest
 * @author <NAME>
 */
class GeoJSONWriterTest {

    @TempDir
    File folder

    @Test void write() {

        // Create a simple Schema
        Schema schema = new Schema("houses", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])

        // Create a Layer in memory with a couple of Features
        Memory memory = new Memory()
        Layer layer = memory.create(schema)
        layer.add(new Feature([new Point(111,-47), "House", 12.5], "house1", schema))
        layer.add(new Feature([new Point(121,-45), "School", 22.7], "house2", schema))

        String expected = ""

        // Write the Layer to a GeoJSON String
        GeoJSONWriter writer = new GeoJSONWriter()
        String geojson = writer.write(layer)
        assertTrue geojson.startsWith("{\"type\":\"FeatureCollection\",\"features\":[")

        // Write Layer as GeoJSON to an OutputStream
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(layer, out)
        out.close()
        geojson = out.toString()
        assertTrue geojson.startsWith("{\"type\":\"FeatureCollection\",\"features\":[")

        // Write Layer as GeoJSON to a File
        File file = new File(folder,"layer.json")
        writer.write(layer, file)
        geojson = file.text
        assertTrue geojson.startsWith("{\"type\":\"FeatureCollection\",\"features\":[")
    }

    @Test void writeWithOptions() {

        // Create a simple Schema
        Schema schema = new Schema("houses", [new Field("geom","Point","EPSG:4326"), new Field("name","string"), new Field("price","float")])

        // Create a Layer in memory with a couple of Features
        Memory memory = new Memory()
        Layer layer = memory.create(schema)
        layer.add(new Feature([new Point(111.123456,-47.123456), "House", 12.5], "house1", schema))
        layer.add(new Feature([new Point(121.123456,-45.123456), "School", 22.7], "house2", schema))

        String expected = ""

        // Write the Layer to a GeoJSON String
        GeoJSONWriter writer = new GeoJSONWriter()
        String geojson = writer.write(layer, decimals: 6, encodeFeatureBounds: true, encodeFeatureCRS: true,
                encodeFeatureCollectionBounds: true, encodeFeatureCollectionCRS: true)
        checkJson(geojson)

        // Write Layer as GeoJSON to an OutputStream
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(layer, out, decimals: 6, encodeFeatureBounds: true, encodeFeatureCRS: true,
                encodeFeatureCollectionBounds: true, encodeFeatureCollectionCRS: true)
        out.close()
        geojson = out.toString()
        checkJson(geojson)

        // Write Layer as GeoJSON to a File
        File file = new File(folder,"layer.json")
        writer.write(layer, file, decimals: 6, encodeFeatureBounds: true, encodeFeatureCRS: true,
                encodeFeatureCollectionBounds: true, encodeFeatureCollectionCRS: true)
        geojson = file.text
        checkJson(geojson)

        // Pretty print
        geojson = writer.write(layer, prettyPrint: true)
        assertTrue geojson.split("\n").length > 1
    }

    private void checkJson(String geojson) {
        JsonSlurper slurper = new JsonSlurper()
        Map json = slurper.parseText(geojson)
        assertEquals("FeatureCollection", json.type)
        assertTrue json.containsKey("bbox")
        assertTrue json.containsKey("crs")
        assertTrue json.containsKey("features")
        List features = json.features
        assertEquals(2, features.size())
        features.each { Map feature ->
            assertEquals("Feature", feature.type)
            assertTrue feature.containsKey("bbox")
            assertTrue feature.containsKey("crs")
            assertTrue feature.containsKey("geometry")
            assertTrue feature.containsKey("properties")
        }
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/io/MvtTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.feature.Field
import geoscript.layer.Layer
import geoscript.layer.Property
import geoscript.layer.Shapefile
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import static org.junit.jupiter.api.Assertions.*

/**
 * The Mvt Unit Test
 * @author <NAME>
 */
class MvtTest {

    @TempDir
    File folder

    @Test void writeRead() {
        URL url = getClass().getClassLoader().getResource("states.shp")
        Layer layer = new Shapefile(new File(url.toURI()))
        File file = new File(folder,"states.mvt")
        Mvt.write(layer, file)
        assertTrue file.length() > 0
        Layer layer2 = Mvt.read(file)
        assertTrue layer2.count > 0
        layer.schema.fields.each { Field fld ->
            if (!fld.isGeometry()) {
                assertTrue layer2.schema.has(fld.name)
            }
        }
    }

    @Test void writeToFile() {
        URL url = getClass().getClassLoader().getResource("states.shp")
        Layer layer = new Shapefile(new File(url.toURI()))
        File file = new File(folder,"states.mvt")
        Mvt.write(layer, file)
        assertTrue file.length() > 0
    }

    @Test void writeToOutputStream() {
        URL url = getClass().getClassLoader().getResource("states.shp")
        Layer layer = new Shapefile(new File(url.toURI()))
        File file = new File(folder,"states.mvt")
        OutputStream out = new FileOutputStream(file)
        Mvt.write(layer, out)
        out.close()
        assertTrue file.length() > 0
    }

    @Test void readFromUrl() {
        URL url = getClass().getClassLoader().getResource("1582.mvt")
        Layer layer = Mvt.read(url)
        assertEquals 6, layer.count
    }

    @Test void readFromFile() {
        URL url = getClass().getClassLoader().getResource("1582.mvt")
        Layer layer = Mvt.read(new File(url.toURI()))
        assertEquals 6, layer.count
    }

    @Test void readFromInputStream() {
        URL url = getClass().getClassLoader().getResource("1582.mvt")
        InputStream input = url.openStream()
        Layer layer = Mvt.read(input)
        input.close()
        assertEquals 6, layer.count
    }

    @Test void writeAndReadEarthquakes() {
        URL url = getClass().getClassLoader().getResource("earthquakes.properties")
        File file = new File(url.toURI())
        Layer inLayer = new Property(file)
        File outFile = new File(folder,"earthquakes.mvt")
        Mvt.write(inLayer, outFile)
        Layer outLayer = Mvt.read(outFile)
        assertEquals inLayer.count, outLayer.count
    }
}


<|start_filename|>src/main/groovy/geoscript/carto/VerticalAlign.groovy<|end_filename|>
package geoscript.carto

/**
 * Vertically align text
 * @author <NAME>
 */
enum VerticalAlign {
    TOP, MIDDLE, BOTTOM
}

<|start_filename|>src/main/groovy/geoscript/geom/CompoundRing.groovy<|end_filename|>
package geoscript.geom

import org.locationtech.jts.geom.LineString as JtsLineString
import org.geotools.geometry.jts.CompoundRing as GtCompoundRing
import org.geotools.geometry.jts.CurvedGeometryFactory

/**
 * A CompoundRing is a connected set of CircularStrings and LineStrings
 * @author <NAME>
 */
class CompoundRing extends LinearRing {

    /**
     * Create a CompoundRing from a GeoTools CompoundRing
     * @param cr The GeoTools CompoundRing
     */
    CompoundRing(GtCompoundRing cr) {
        super(cr)
    }

    /**
     * Create a CompoundRing from a List of repeated LineStrings or CircularStrings
     * @param options The optional named parameters
     * <ul>
     *     <li>tolerance = The distance tolerance used to linearize the curve. Defaults to Double.MAX_VALUE. </li>
     * </ul>
     * @param lineStrings The List of repeated LineStrings or CircularStrings
     */
    CompoundRing(Map options = [:], LineString... lineStrings) {
        this(create(lineStrings.collect{it}, options.get("tolerance", Double.MAX_VALUE)))
    }

    /**
     * Create a CompoundRing from a List of LineStrings or CircularStrings
     * @param options The optional named parameters
     * <ul>
     *     <li>tolerance = The distance tolerance used to linearize the curve. Defaults to Double.MAX_VALUE. </li>
     * </ul>
     * @param lineStrings The List of LineStrings or CircularStrings
     */
    CompoundRing(Map options = [:], List<LineString> lineStrings) {
        this(create(lineStrings, options.get("tolerance", Double.MAX_VALUE)))
    }

    /**
     * Create a GeoTools CompoundRing from a List of GeoScript LineStrings or CircularStrings
     * @param lineStrings The List of GeoScript LineStrings or CircularStrings
     * @param tolerance The distance tolerance used to linearize the curve.
     * @return A GeoTools CompoundRing
     */
    private static create(List<LineString> lineStrings, double tolerance) {
        CurvedGeometryFactory cgf = new CurvedGeometryFactory(tolerance)
        new GtCompoundRing(lineStrings.collect{it.g as JtsLineString}, cgf, tolerance)
    }

    /**
     * Get the curved WKT
     * @return The curved WKT
     */
    String getCurvedWkt() {
        (g as GtCompoundRing).toCurvedText()
    }

    /**
     * Get the original LineStrings or CircularStrings (not linearized)
     * @return The original LineStrings or CircularStrings
     */
    List<LineString> getComponents() {
        (g as GtCompoundRing).components.collect{Geometry.wrap(it)}
    }

    /**
     * Get the linearized Geometry
     * @return The linearized Geometry
     */
    Geometry getLinear() {
        Geometry.wrap((g as GtCompoundRing).linearize())
    }

}


<|start_filename|>src/test/groovy/geoscript/layer/io/CsvWriterTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.proj.Projection
import org.junit.jupiter.api.Test
import geoscript.feature.Schema
import geoscript.feature.Field
import geoscript.workspace.Memory
import geoscript.layer.Layer
import geoscript.feature.Feature
import geoscript.geom.Point
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.assertEquals

/**
 * The CsvWriter Unit Test
 * @author <NAME>
 */
class CsvWriterTest {

    @TempDir
    File folder

    private Layer createLayer(Projection proj = null) {
        // Create a simple Schema
        Schema schema = new Schema("houses", [new Field("geom","Point", proj), new Field("name","string"), new Field("price","float")])

        // Create a Layer in memory with a couple of Features
        Memory memory = new Memory()
        Layer layer = memory.create(schema)
        layer.add(new Feature([new Point(111,-47), "House", 12.5], "house1", schema))
        layer.add(new Feature([new Point(121,-45), "School", 22.7], "house2", schema))
        layer
    }

    @Test void writeWTK() {

        Layer layer = createLayer()

        String expected = """"geom","name","price"
"POINT (111 -47)","House","12.5"
"POINT (121 -45)","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(encodeFieldType: false)
        String csv = writer.write(layer)
        assertEquals(expected, csv)

        // Write Layer as CSV to an OutputStream
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(layer, out)
        out.close()
        csv = out.toString()
        assertEquals(expected, csv)

        // Write Layer as CSV to a File
        File file = new File(folder,"layer.csv")
        writer.write(layer, file)
        csv = file.text
        assertEquals(expected, csv)
    }

    @Test void writeWTKWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point","name:String","price:Float"
"POINT (111 -47)","House","12.5"
"POINT (121 -45)","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter()
        String csv = writer.write(layer)
        assertEquals(expected, csv)

        // Write Layer as CSV to an OutputStream
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(layer, out)
        out.close()
        csv = out.toString()
        assertEquals(expected, csv)

        // Write Layer as CSV to a File
        File file = new File(folder,"layer.csv")
        writer.write(layer, file)
        csv = file.text
        assertEquals(expected, csv)
    }

    @Test void writeWTKWithTypesAndProjection() {

        Layer layer = createLayer(new Projection("EPSG:4326"))

        String expected = """"geom:Point:EPSG:4326","name:String","price:Float"
"POINT (111 -47)","House","12.5"
"POINT (121 -45)","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter()
        String csv = writer.write(layer)
        assertEquals(expected, csv)

        // Write Layer as CSV to an OutputStream
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(layer, out)
        out.close()
        csv = out.toString()
        assertEquals(expected, csv)

        // Write Layer as CSV to a File
        File file = new File(folder,"layer.csv")
        writer.write(layer, file)
        csv = file.text
        assertEquals(expected, csv)
    }

    @Test void writeXY() {

        Layer layer = createLayer()

        String expected = """"lon"|"lat"|"name"|"price"
"111.0"|"-47.0"|"House"|"12.5"
"121.0"|"-45.0"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat", separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)

        // Write Layer as CSV to an OutputStream
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(layer, out)
        out.close()
        csv = out.toString()
        assertEquals(expected, csv)

        // Write Layer as CSV to a File
        File file = new File(folder,"layer.csv")
        writer.write(layer, file)
        csv = file.text
        assertEquals(expected, csv)
    }

    @Test void writeXYWithTypes() {

        Layer layer = createLayer()

        String expected = """"lon:Double"|"lat:Double"|"name:String"|"price:Float"
"111.0"|"-47.0"|"House"|"12.5"
"121.0"|"-45.0"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat", separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)

        // Write Layer as CSV to an OutputStream
        ByteArrayOutputStream out = new ByteArrayOutputStream()
        writer.write(layer, out)
        out.close()
        csv = out.toString()
        assertEquals(expected, csv)

        // Write Layer as CSV to a File
        File file = new File(folder,"layer.csv")
        writer.write(layer, file)
        csv = file.text
        assertEquals(expected, csv)
    }

    @Test void writeXYInSingleColumn() {

        Layer layer = createLayer()

        String expected = """"geom"|"name"|"price"
"111.0,-47.0"|"House"|"12.5"
"121.0,-45.0"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.XY, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeXYInSingleColumnWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point"|"name:String"|"price:Float"
"111.0,-47.0"|"House"|"12.5"
"121.0,-45.0"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.XY, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMS() {

        Layer layer = createLayer()

        String expected = """"lon"|"lat"|"name"|"price"
"111\u00B0 0' 0.0000"" W"|" -47\u00B0 0' 0.0000"" N"|"House"|"12.5"
"121\u00B0 0' 0.0000"" W"|" -45\u00B0 0' 0.0000"" N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DMS, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMSWithTypes() {

        Layer layer = createLayer()

        String expected = """"lon:String"|"lat:String"|"name:String"|"price:Float"
"111\u00B0 0' 0.0000"" W"|" -47\u00B0 0' 0.0000"" N"|"House"|"12.5"
"121\u00B0 0' 0.0000"" W"|" -45\u00B0 0' 0.0000"" N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DMS, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMSInSingleColumn() {

        Layer layer = createLayer()

        String expected = """"geom"|"name"|"price"
"111\u00B0 0' 0.0000"" W, -47\u00B0 0' 0.0000"" N"|"House"|"12.5"
"121\u00B0 0' 0.0000"" W, -45\u00B0 0' 0.0000"" N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DMS, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMSInSingleColumnWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point"|"name:String"|"price:Float"
"111\u00B0 0' 0.0000"" W, -47\u00B0 0' 0.0000"" N"|"House"|"12.5"
"121\u00B0 0' 0.0000"" W, -45\u00B0 0' 0.0000"" N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DMS, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMSChar() {

        Layer layer = createLayer()

        String expected = """"lon"|"lat"|"name"|"price"
"111d 0m 0.0000s W"|" -47d 0m 0.0000s N"|"House"|"12.5"
"121d 0m 0.0000s W"|" -45d 0m 0.0000s N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DMSChar, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMSCharWithTypes() {

        Layer layer = createLayer()

        String expected = """"lon:String"|"lat:String"|"name:String"|"price:Float"
"111d 0m 0.0000s W"|" -47d 0m 0.0000s N"|"House"|"12.5"
"121d 0m 0.0000s W"|" -45d 0m 0.0000s N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DMSChar, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMSCharInSingleColumn() {

        Layer layer = createLayer()

        String expected = """"geom"|"name"|"price"
"111d 0m 0.0000s W, -47d 0m 0.0000s N"|"House"|"12.5"
"121d 0m 0.0000s W, -45d 0m 0.0000s N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DMSChar, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDMSCharInSingleColumnWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point"|"name:String"|"price:Float"
"111d 0m 0.0000s W, -47d 0m 0.0000s N"|"House"|"12.5"
"121d 0m 0.0000s W, -45d 0m 0.0000s N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DMSChar, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDM() {

        Layer layer = createLayer()

        String expected = """"lon"|"lat"|"name"|"price"
"111\u00B0 0.0000' W"|" -47\u00B0 0.0000' N"|"House"|"12.5"
"121\u00B0 0.0000' W"|" -45\u00B0 0.0000' N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DDM, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDMWithTypes() {

        Layer layer = createLayer()

        String expected = """"lon:String"|"lat:String"|"name:String"|"price:Float"
"111\u00B0 0.0000' W"|" -47\u00B0 0.0000' N"|"House"|"12.5"
"121\u00B0 0.0000' W"|" -45\u00B0 0.0000' N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DDM, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDMInSingleColumn() {

        Layer layer = createLayer()

        String expected = """"geom"|"name"|"price"
"111\u00B0 0.0000' W, -47\u00B0 0.0000' N"|"House"|"12.5"
"121\u00B0 0.0000' W, -45\u00B0 0.0000' N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DDM, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDMInSingleColumnWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point"|"name:String"|"price:Float"
"111\u00B0 0.0000' W, -47\u00B0 0.0000' N"|"House"|"12.5"
"121\u00B0 0.0000' W, -45\u00B0 0.0000' N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DDM, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDMChar() {

        Layer layer = createLayer()

        String expected = """"lon"|"lat"|"name"|"price"
"111d 0.0000m W"|" -47d 0.0000m N"|"House"|"12.5"
"121d 0.0000m W"|" -45d 0.0000m N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DDMChar, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDMCharWithTypes() {

        Layer layer = createLayer()

        String expected = """"lon:String"|"lat:String"|"name:String"|"price:Float"
"111d 0.0000m W"|" -47d 0.0000m N"|"House"|"12.5"
"121d 0.0000m W"|" -45d 0.0000m N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter("lon","lat",CsvWriter.Type.DDMChar, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDMCharInSingleColumn() {

        Layer layer = createLayer()

        String expected = """"geom"|"name"|"price"
"111d 0.0000m W, -47d 0.0000m N"|"House"|"12.5"
"121d 0.0000m W, -45d 0.0000m N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DDMChar, separator: "|")
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeDDMCharInSingleColumnWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point"|"name:String"|"price:Float"
"111d 0.0000m W, -47d 0.0000m N"|"House"|"12.5"
"121d 0.0000m W, -45d 0.0000m N"|"School"|"22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.DDMChar, separator: "|", encodeFieldType: true)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeWTB() {

        Layer layer = createLayer()

        String expected = """"geom","name","price"
"0000000001405BC00000000000C047800000000000","House","12.5"
"0000000001405E400000000000C046800000000000","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.WKB, encodeFieldType: false)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeWTBWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point","name:String","price:Float"
"0000000001405BC00000000000C047800000000000","House","12.5"
"0000000001405E400000000000C046800000000000","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.WKB)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeGeoJSON() {

        Layer layer = createLayer()

        String expected = """"geom","name","price"
"{""type"":""Point"",""coordinates"":[111,-47]}","House","12.5"
"{""type"":""Point"",""coordinates"":[121,-45]}","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.GEOJSON, encodeFieldType: false)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeGeoJSONWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point","name:String","price:Float"
"{""type"":""Point"",""coordinates"":[111,-47]}","House","12.5"
"{""type"":""Point"",""coordinates"":[121,-45]}","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.GEOJSON)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeKML() {

        Layer layer = createLayer()

        String expected = """"geom","name","price"
"<Point><coordinates>111.0,-47.0</coordinates></Point>","House","12.5"
"<Point><coordinates>121.0,-45.0</coordinates></Point>","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.KML, encodeFieldType: false)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeKMLWithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point","name:String","price:Float"
"<Point><coordinates>111.0,-47.0</coordinates></Point>","House","12.5"
"<Point><coordinates>121.0,-45.0</coordinates></Point>","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.KML)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeGML2() {

        Layer layer = createLayer()

        String expected = """"geom","name","price"
"<gml:Point><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point>","House","12.5"
"<gml:Point><gml:coordinates>121.0,-45.0</gml:coordinates></gml:Point>","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.GML2, encodeFieldType: false)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeGML2WithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point","name:String","price:Float"
"<gml:Point><gml:coordinates>111.0,-47.0</gml:coordinates></gml:Point>","House","12.5"
"<gml:Point><gml:coordinates>121.0,-45.0</gml:coordinates></gml:Point>","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.GML2)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeGML3() {

        Layer layer = createLayer()

        String expected = """"geom","name","price"
"<gml:Point><gml:pos>111.0 -47.0</gml:pos></gml:Point>","House","12.5"
"<gml:Point><gml:pos>121.0 -45.0</gml:pos></gml:Point>","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.GML3, encodeFieldType: false)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    @Test void writeGML3WithTypes() {

        Layer layer = createLayer()

        String expected = """"geom:Point","name:String","price:Float"
"<gml:Point><gml:pos>111.0 -47.0</gml:pos></gml:Point>","House","12.5"
"<gml:Point><gml:pos>121.0 -45.0</gml:pos></gml:Point>","School","22.7"
"""
        // Write the Layer to a CSV String
        CsvWriter writer = new CsvWriter(CsvWriter.Type.GML3)
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }

    private Layer createLayerWithNewLines(Projection proj = null) {
        // Create a simple Schema
        Schema schema = new Schema("houses", [new Field("geom","Point", proj), new Field("name","string"), new Field("price","float")])

        // Create a Layer in memory with a couple of Features
        Memory memory = new Memory()
        Layer layer = memory.create(schema)
        layer.add(new Feature([new Point(111,-47), "House\n", 12.5], "house1", schema))
        layer.add(new Feature([new Point(121,-45), "School\ris\r\non vacation", 22.7], "house2", schema))
        layer
    }

    @Test void writeWithNewLines() {
        Layer layer = createLayerWithNewLines()
        String expected = """"geom:Point","name:String","price:Float"
"POINT (111 -47)","House ","12.5"
"POINT (121 -45)","School is on vacation","22.7"
"""
        CsvWriter writer = new CsvWriter()
        String csv = writer.write(layer)
        assertEquals(expected, csv)
    }
}


<|start_filename|>src/test/groovy/geoscript/style/TransformTest.groovy<|end_filename|>
package geoscript.style

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.filter.*
import geoscript.process.Process
import geoscript.geom.GeometryCollection
import geoscript.layer.Layer

/**
 * The Transform Unit Test
 * @author <NAME>
 */
class TransformTest {

    @Test void constructors() {

        Transform transform = new Transform("centroid(the_geom)")
        assertNotNull transform.function
        assertEquals "Transform(function = centroid([the_geom]))", transform.toString()
        assertEquals transform.type, Transform.Type.NORMAL

        Transform transform1 = new Transform(new Function("myCentroid(the_geom)", {g -> g.centroid}))
        assertNotNull transform1.function
        assertEquals "Transform(function = myCentroid([the_geom]))", transform1.toString()
        assertEquals transform1.type, Transform.Type.NORMAL

        Transform transform3 = new Transform(new Function("centroid(the_geom)"))
        assertNotNull transform3.function
        assertEquals "Transform(function = centroid([the_geom]))", transform3.toString()
        assertEquals transform3.type, Transform.Type.NORMAL

        Process p = new Process("convexhull",
                "Create a convexhull around the features",
                [features: geoscript.layer.Cursor],
                [result: geoscript.layer.Cursor],
                { inputs ->
                    def geoms = new GeometryCollection(inputs.features.collect{f -> f.geom})
                    def output = new Layer()
                    output.add([geoms.convexHull])
                    [result: output]
                }
        )
        Function f = new Function(p, new Function("parameter", new Expression("features")))
        Transform transform4 = new Transform(f, Transform.RENDERING)
        assertEquals transform4.type, Transform.Type.RENDERING
    }

    @Test void appy() {
        def pointSym = Symbolizer.styleFactory.createPointSymbolizer();
        Transform centroidTransform = new Transform(new Function("myCentroid(the_geom)", {g -> g.centroid}))
        centroidTransform.apply(pointSym)
        assertTrue pointSym.geometry instanceof org.opengis.filter.expression.Function

        def textSym = Symbolizer.styleFactory.createTextSymbolizer()
        Transform upperCaseTransform = new Transform(new Function("myUpperCase(NAME)", {str -> str.toUpperCase()}))
        upperCaseTransform.apply(textSym)
        assertTrue textSym.label instanceof org.opengis.filter.expression.Function
    }

    @Test void prepare() {
        def rule = Symbolizer.styleFactory.createRule()
        rule.symbolizers().add(Symbolizer.styleFactory.createPointSymbolizer())
        rule.symbolizers().add(Symbolizer.styleFactory.createTextSymbolizer())

        Transform centroidTransform = new Transform(new Function("myCentroid(the_geom)", {g -> g.centroid}))
        Transform upperCaseTransform = new Transform(new Function("myUpperCase(NAME)", {str -> str.toUpperCase()}))
        centroidTransform.prepare(rule)
        upperCaseTransform.prepare(rule)

        assertTrue rule.symbolizers()[0].geometry instanceof org.opengis.filter.expression.Function
        assertTrue rule.symbolizers()[1].label instanceof org.opengis.filter.expression.Function
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/NetCDFTest.groovy<|end_filename|>
package geoscript.layer

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The NetCDF Test Case
 */
class NetCDFTest {

    @Test void read() {
        File file = new File(getClass().getClassLoader().getResource("O3-NO2.nc").toURI())
        NetCDF netcdf = new NetCDF(file)
        assertNotNull(netcdf)
        assertEquals "NetCDF", netcdf.name
        assertNotNull(netcdf.read("O3"))
        assertNotNull(netcdf.read("NO2"))
    }

    @Test void getNames() {
        File file = new File(getClass().getClassLoader().getResource("O3-NO2.nc").toURI())
        NetCDF netcdf = new NetCDF(file)
        assertNotNull(netcdf)
        List names = netcdf.names
        assertEquals(2, names.size())
        assertTrue(names.contains("O3"))
        assertTrue(names.contains("NO2"))
        netcdf.names.each{ String name ->
            Raster raster = netcdf.read(name)
            assertNotNull raster
            assertNotNull raster.proj
            assertNotNull raster.bounds
            raster.dispose()
        }
    }
}


<|start_filename|>src/test/groovy/geoscript/layer/GeoTIFFTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.*

/**
 * The Raster unit test
 */
class GeoTIFFTest {

    @Test
    void geotiff() {

        File file = new File(getClass().getClassLoader().getResource("alki.tif").toURI())
        assertNotNull(file)

        GeoTIFF geoTIFF = new GeoTIFF(file)
        assertNotNull(geoTIFF)
        assertNotNull(geoTIFF.stream)
        assertEquals("GeoTIFF", geoTIFF.name)

        def fmt = Format.getFormat(file)
        assertTrue(fmt instanceof GeoTIFF)
        assertNotNull(fmt.stream)

        Raster raster = geoTIFF.read()
        assertEquals("EPSG:2927", raster.proj.id)

        Bounds bounds = raster.bounds
        assertEquals(1166191.0260847565, bounds.minX, 0.0000000001)
        assertEquals(1167331.8522748263, bounds.maxX, 0.0000000001)
        assertEquals(822960.0090852415, bounds.minY, 0.0000000001)
        assertEquals(824226.3820666744, bounds.maxY, 0.0000000001)
        assertEquals("EPSG:2927", raster.proj.id)

        def (int w, int h) = raster.size
        assertEquals(761, w)
        assertEquals(844, h)

        List<Band> bands = raster.bands
        assertEquals(3, bands.size())
        assertEquals("RED_BAND", bands[0].toString())
        assertEquals("GREEN_BAND", bands[1].toString())
        assertEquals("BLUE_BAND", bands[2].toString())

        def (int bw, int bh) = raster.blockSize
        assertEquals(761, bw)
        assertEquals(3, bh)

        def (double pw, double ph) = raster.pixelSize
        assertEquals(1.4991145730220545, pw, 0.000000000001)
        assertEquals(1.5004419211290778, ph, 0.000000000001)
    }
}

<|start_filename|>src/main/groovy/geoscript/render/io/XmlMapReader.groovy<|end_filename|>
package geoscript.render.io

import geoscript.geom.Bounds
import geoscript.layer.Renderables
import geoscript.render.Map as GMap
import groovy.xml.XmlSlurper
import groovy.xml.slurpersupport.GPathResult

/**
 * Read a Map from an XML String.
 * <pre>
 * {@code
 * <map>
 *   <width>400</width>
 *   <height>400</height>
 *   <type>png</type>
 *   <proj>EPSG:4326</proj>
 *   <backgroundColor>blue</backgroundColor>
 *   <fixAspectRatio>true</fixAspectRatio>
 *   <layers>
 *       <layer>
 *           <layertype>layer</layertype>
 *           <file>states.shp</file>
 *       </layer>
 *   </layers>
 *   <bounds>
 *       <minX>-135.911779</minX>
 *       <minY>36.993573</minY>
 *       <maxX>-96.536779</maxX>
 *       <maxY>51.405899</maxY>
 *   </bounds>
 * </map>
 * }
 * </pre>
 * @author <NAME>
 */
class XmlMapReader implements MapReader {

    @Override
    String getName() {
        "xml"
    }

    @Override
    GMap read(String str) {
        XmlSlurper xmlSlurper = new XmlSlurper()
        GPathResult xml = xmlSlurper.parseText(str)
        read(xml)
    }

    GMap read(GPathResult xml) {
        GMap map = new GMap(
                width: getInt(xml.width?.text()?.toString(), 600),
                height: getInt(xml.height?.text()?.toString(), 400),
                type: xml.type?.text() ?: "png",
                backgroundColor: xml.backgroundColor?.text(),
                fixAspectRatio: getBoolean(xml.fixAspectRatio?.text(), true),
                layers: Renderables.getRenderables(getLayerMaps(xml.layers))
        )
        if (xml.proj?.text()) {
            map.proj = xml.proj.text()
        }
        if (xml.bounds?.text()) {
            Map bounds = [
                    minX: xml.bounds.minX.text() as double,
                    minY: xml.bounds.minY.text() as double,
                    maxX: xml.bounds.maxX.text() as double,
                    maxY: xml.bounds.maxY.text() as double
            ]
            map.bounds = new Bounds(bounds.minX, bounds.minY, bounds.maxX, bounds.maxY, bounds?.proj)
        }
        map
    }

    private int getInt(String s, int defaultValue) {
        if (s) {
            s.toInteger()
        } else {
            defaultValue
        }
    }

    private boolean getBoolean(String s, boolean defaultValue) {
        if (s) {
            Boolean.parseBoolean(s)
        } else {
            defaultValue
        }
    }

    private List<Map> getLayerMaps(def xml) {
        xml.children().collect { def layerDef ->
            Map layerMap = [:]
            layerDef.children().each { def layer ->
                layerMap[layer.name()] = layer.text()
            }
            layerMap
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/io/XmlCartoReaderTest.groovy<|end_filename|>
package geoscript.carto.io

import geoscript.carto.CartoBuilder
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

class XmlCartoReaderTest {

    @Test void getName() {
        CartoReader cartoReader = new XmlCartoReader()
        assertEquals("xml", cartoReader.name)
    }

    @Test void readCarto() {
        String xml = """<carto>
          <type>png</type>
          <width>792</width>
          <height>612</height>
          <items>
            <item>
                <x>0</x>
                <y>0</y>
                <width>792</width>
                <height>612</height>
                <type>rectangle</type>
                <fillColor>white</fillColor>
                <strokeColor>white</strokeColor>
            </item>
            <item>
                <x>0</x>
                <y>0</y>
                <width>792</width>
                <height>612</height>
                <type>grid</type>
                <size>10</size>
                <strokeColor>black</strokeColor>
                <strokeWidth>1</strokeWidth>
            </item>
            <item>
                <x>10</x>
                <y>10</y>
                <width>772</width>
                <height>592</height>
                <type>rectangle</type>
                <strokeWidth>1.0</strokeWidth>
                <strokeColor>black</strokeColor>
                <fillColor>white</fillColor>
                <strokeWidth>1</strokeWidth>
            </item>
            <item>
                <x>20</x>
                <y>20</y>
                <width>752</width>
                <height>80</height>
                <type>rectangle</type>
            </item>
            <item>
                <x>30</x>
                <y>50</y>
                <width>200</width>
                <height>20</height>
                <type>text</type>
                <text>Map Title</text>
                <font>
                    <name>Arial</name>
                    <style>Bold</style>
                    <size>36</size>
                </font>
                <color>black</color>
                <horizontalAlign>left</horizontalAlign>
                <verticalAlign>bottom</verticalAlign>
            </item>
            <item>
                <x>30</x>
                <y>85</y>
                <width>200</width>
                <height>10</height>
                <type>dateText</type>
                <format>MM/dd/yyyy</format>
                <date>12/29/2020</date>
                <color>black</color>
                <horizontalAlign>left</horizontalAlign>
                <verticalAlign>bottom</verticalAlign>
                <font>
                    <name>Arial</name>
                    <style>Italic</style>
                    <size>18</size>
                </font>
            </item>
            <item>
                <x>250</x>
                <y>30</y>
                <width>380</width>
                <height>70</height>
                <type>paragraph</type>
                <text>Permission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the Software), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.</text>
                <font>
                    <name>Arial</name>
                    <style>Plain</style>
                    <size>8</size>
                </font>
                <color>black</color>
            </item>
            <item>
                <x>710</x>
                <y>30</y>
                <width>1</width>
                <height>60</height>
                <type>line</type>
                <strokeColor>black</strokeColor>
                <strokeWidth>1</strokeWidth>
            </item>
            <item>
                <x>640</x>
                <y>30</y>
                <width>60</width>
                <height>60</height>
                <type>image</type>
                <path>${new File(getClass().getClassLoader().getResource("image.png").toURI()).absolutePath}</path>
            </item>
            <item>
                <x>720</x>
                <y>30</y>
                <width>40</width>
                <height>60</height>
                <type>northArrow</type>
                <fillColor1>black</fillColor1>
                <strokeColor1>black</strokeColor1>
                <fillColor2>white</fillColor2>
                <strokeColor2>black</strokeColor2>
                <strokeWidth>1</strokeWidth>
                <drawText>false</drawText>
                <font>
                    <name>Arial</name>
                    <style>BOLD</style>
                    <size>48</size>
                </font>
                <textColor>black</textColor>
            </item>
            <item>
                <x>20</x>
                <y>110</y>
                <width>752</width>
                <height>480</height>
                <type>rectangle</type>
            </item>
            <item>
                <x>30</x>
                <y>120</y>
                <width>742</width>
                <height>470</height>
                <type>map</type>
                <name>mainMap</name>
                <layers>
                    <layer>
                        <layertype>layer</layertype>
                        <file>${new File("src/test/resources/states.shp").absolutePath}</file>
                    </layer>
                </layers>
            </item>
            <item>
                <x>30</x>
                <y>490</y>
                <width>100</width>
                <height>90</height>
                <type>overViewMap</type>
                <linkedMap>mainMap</linkedMap>
                <layers>
                    <layer>
                        <layertype>raster</layertype>
                        <source>${new File("src/test/resources/raster.tif").absolutePath}</source>
                    </layer>
                </layers>
            </item>
            <item>
                <x>150</x>
                <y>490</y>
                <width>100</width>
                <height>90</height>
                <type>overViewMap</type>
                <linkedMap>mainMap</linkedMap>
                <zoomIntoBounds>true</zoomIntoBounds>
                <scaleFactor>3.0</scaleFactor>
                <layers>
                    <layer>
                        <layertype>raster</layertype>
                        <source>${new File("src/test/resources/raster.tif").absolutePath}</source>
                    </layer>
                </layers>
            </item>
            <item>
                <x>460</x>
                <y>120</y>
                <width>300</width>
                <height>200</height>
                <type>table</type>
                <columns>
                    <column>ID</column>
                    <column>Name</column>
                </columns>
                <rows>
                    <row><ID>1</ID><Name>One</Name></row>
                    <row><ID>2</ID><Name>Two</Name></row>
                    <row><ID>3</ID><Name>Three</Name></row>
                </rows>
            </item>
            <item>
                <x>640</x>
                <y>500</y>
                <width>120</width>
                <height>80</height>
                <type>legend</type>
                <map>mainMap</map>
                <backgroundColor>white</backgroundColor>
                <title>Legend</title>
                <titleFont>
                    <name>Arial</name>
                    <style>bold</style>
                    <size>18</size>
                </titleFont>
                <titleColor>black</titleColor>
                <textFont>
                    <name>Arial</name>
                    <style>plain</style>
                    <size>12</size>
                </textFont>
                <textColor>black</textColor>
                <legendEntryWidth>50</legendEntryWidth>
                <legendEntryHeight>30</legendEntryHeight>
                <gapBetweenEntries>10</gapBetweenEntries>
                <numberFormat>#.##</numberFormat>
            </item>
            <item>
                <x>150</x>
                <y>85</y>
                <width>200</width>
                <height>10</height>
                <type>scaleText</type>
                <map>mainMap</map>
                <font>
                    <name>Arial</name>
                    <style>italic</style>
                    <size>18</size>
                </font>
                <color>black</color>
                <horizontalAlign>left</horizontalAlign>
                <verticalAlign>bottom</verticalAlign>
                <format>#</format>
                <prefixText>Scale :</prefixText>
            </item>
            <item>
                <x>50</x>
                <y>125</y>
                <width>200</width>
                <height>20</height>
                <type>scaleBar</type>
                <map>mainMap</map>
                <strokeColor>black</strokeColor>
                <fillColor>white</fillColor>
                <strokeWidth>1</strokeWidth>
                <font>
                    <name>Arial</name>
                    <style>italic</style>
                    <size>12</size>
                </font>
                <border>1</border>
                <units>metric</units>
            </item>
          </items>
        </carto>"""
        CartoReader cartoReader = new XmlCartoReader()
        CartoBuilder cartoBuilder = cartoReader.read(xml)
        boolean saveToTarget = false
        File file = saveToTarget ? new File("target/carto_xml.png"): File.createTempFile("carto_xml","png")
        file.withOutputStream { OutputStream outputStream ->
            cartoBuilder.build(outputStream)
        }
    }

}


<|start_filename|>src/test/groovy/geoscript/plot/BarTest.groovy<|end_filename|>
package geoscript.plot

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import static org.junit.jupiter.api.Assertions.*

/**
 * The Bar Unit Test
 * @author <NAME>
 */
class BarTest {

    @TempDir
    File folder

    @Test void xy() {
        List data = [
            [1,10],[45,12],[23,3],[5,20]
        ]
        Chart chart = Bar.xy(data)
        File file = new File(folder,"xy.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void category() {
        Map data = [
            "A":20,"B":45,"C":2,"D":14
        ]
        Chart chart = Bar.category(data)
        File file = new File(folder,"category.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void categoryStacked() {
        Map data = [
                "A": ["B":45,"C":2,"D":14],
                "F": ["G":45,"H":2,"I":14]
        ]
        Chart chart = Bar.category(data, stacked: true)
        File file = new File(folder,"categoryStacked.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void category3DStacked() {
        Map data = [
                "A": ["B":45,"C":2,"D":14],
                "F": ["G":45,"H":2,"I":14]
        ]
        Chart chart = Bar.category(data, trid: true, stacked: true)
        File file = new File(folder,"category3Dstacked.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void category3D() {
        Map data = [
                "A":20,"B":45,"C":2,"D":14
        ]
        Chart chart = Bar.category(data, trid: true)
        File file = new File(folder,"category3D.png")
        chart.save(file)
        assertTrue(file.exists())
    }

    @Test void categorySeries() {
        Map data = [
                "A": ["B":45,"C":2,"D":14],
                "F": ["G":45,"H":2,"I":14],
        ]
        Chart chart = Bar.category(data)
        File file = new File(folder,"categorySeries.png")
        chart.save(file)
        assertTrue(file.exists())
    }

}


<|start_filename|>src/test/groovy/geoscript/proj/GeodeticTest.groovy<|end_filename|>
package geoscript.proj

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.Point

/**
 * The Geodetic UnitTest
 * @author <NAME>
 */
class GeodeticTest {

    @Test void constructors() {
        def g1 = new Geodetic()
        assertEquals Geodetic.WGS84, g1.ellipsoid

        def g2 = new Geodetic("clrk66")
        assertEquals Geodetic.CLRK66, g2.ellipsoid

        def g3 = new Geodetic(Geodetic.MERIT)
        assertEquals Geodetic.MERIT, g3.ellipsoid
    }

    @Test void inverse() {
        def g = new Geodetic("clrk66")

        double boston_lat = 42.0+(15.0/60.0)
        double boston_lon = -71.0-(7.0/60.0)
        Point bostonPoint = new Point(boston_lon, boston_lat)

        double portland_lat = 45.0+(31.0/60.0)
        double portland_lon = -123.0-(41.0/60.0)
        Point portlandPoint = new Point(portland_lon, portland_lat)

        def inverse = g.inverse(bostonPoint, portlandPoint)
        assertEquals(-66.531, inverse.forwardAzimuth, 0.001)
        assertEquals(75.654, inverse.backAzimuth, 0.001)
        assertEquals(4164192.708, inverse.distance, 0.001)
    }

    @Test void forward() {
        def g = new Geodetic("clrk66")

        double boston_lat = 42.0+(15.0/60.0)
        double boston_lon = -71.0-(7.0/60.0)
        Point bostonPoint = new Point(boston_lon, boston_lat)
        
        double portland_lat = 45.0+(31.0/60.0)
        double portland_lon = -123.0-(41.0/60.0)
        Point portlandPoint = new Point(portland_lon, portland_lat)

        def forward = g.forward(bostonPoint, -66.531, 4164192.708)
        // 45.517  -123.683        75.654
        assertEquals(45.517, forward.point.y, 0.001)
        assertEquals(-123.683, forward.point.x, 0.001)
        assertEquals(75.654, forward.backAzimuth, 0.001)
    }

    @Test void placePoints() {
        def g = new Geodetic("clrk66")

        double boston_lat = 42.0+(15.0/60.0)
        double boston_lon = -71.0-(7.0/60.0)
        Point bostonPoint = new Point(boston_lon, boston_lat)

        
        double portland_lat = 45.0+(31.0/60.0)
        double portland_lon = -123.0-(41.0/60.0)
        Point portlandPoint = new Point(portland_lon, portland_lat)

        def points = g.placePoints(bostonPoint, portlandPoint, 10)
        assertEquals 10, points.size()

        def expectedPoints = [
            new Point(-75.414, 43.528), 
            new Point(-79.883, 44.637),
            new Point(-84.512, 45.565), 
            new Point(-89.279, 46.299), 
            new Point(-94.156, 46.830), 
            new Point(-99.112, 47.149), 
            new Point(-104.106, 47.251), 
            new Point(-109.100, 47.136), 
            new Point(-114.051, 46.805), 
            new Point(-118.924, 46.262) 
        ]

        points.eachWithIndex{actual, i->
            def expected = expectedPoints[i]
            assertEquals(expected.x, actual.x, 0.001)
            assertEquals(expected.y, actual.y, 0.001)
        }
    }

    @Test void string() {
        assertEquals """Geodetic [SPHEROID["WGS 84", 6378137.0, 298.257223563]]""", (new Geodetic()).toString()
    }
}


<|start_filename|>src/main/groovy/geoscript/layer/Pyramid.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import geoscript.geom.Point
import geoscript.layer.io.PyramidReader
import geoscript.layer.io.PyramidReaders
import geoscript.proj.Projection
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

/**
 * The Tile Pyramid
 * @author <NAME>
 */
@EqualsAndHashCode
@ToString(includeNames = true, excludes = ["grids", "minGrid", "maxGrid"])
class Pyramid {

    /**
     * The Projection
     */
    Projection proj = new Projection("EPSG:4326")

    /**
     * The Bounds
     */
    Bounds bounds = new Bounds(-179.99, -90, 179.99, 90, "EPSG:4326")

    /**
     * The Origin
     */
    Origin origin = Origin.BOTTOM_LEFT

    /**
     * The tile width
     */
    int tileWidth = 256

    /**
     * The tile height
     */
    int tileHeight = 256

    /**
     * The List of Grids
     */
    List<Grid> grids = []

    /**
     * The Origin enumeration
     */
    static enum Origin {
        BOTTOM_LEFT,
        TOP_LEFT,
        BOTTOM_RIGHT,
        TOP_RIGHT
    }

    /**
     * Find the Grid for the given zoom level
     * @param z The zoom level
     * @return The Grid or null
     */
    Grid grid(long z) {
        grids.find { Grid m ->
            m.z == z
        }
    }

    /**
     * Find the best Grid for the given Bounds and XY resolutions
     * @param b The Bounds
     * @param resX The x resolution
     * @param resY The y resolution
     * @return A best Grid or null
     */
    Grid grid(Bounds b, double resX, double resY) {
        Grid best = null
        double score = Double.MAX_VALUE
        grids.each { Grid m ->
            double res = Math.abs(resX - m.xResolution) + Math.abs(resY - m.yResolution)
            if (res < score) {
                score = res
                best = m
            }
        }
        best
    }

    /**
     * Find the best Grid for the given Bounds and image width and height
     * @param b The Bounds
     * @param w The image width
     * @param h The image height
     * @return The best Grid or null
     */
    Grid grid(Bounds b, int w, int h) {
        double resX = b.width / (w as double)
        double resY = b.height / (h as double)
        grid(b, resX, resY)
    }

    /**
     * Get the max Grid by zoom level
     * @return THe max Grid by zoom level
     */
    Grid getMaxGrid() {
        grids.max { Grid g -> g.z }
    }

    /**
     * Get the min Grid by zoom level
     * @return The min Grid by zoom level
     */
    Grid getMinGrid() {
        grids.min { Grid g -> g.z }
    }

    /**
     * Find the Bounds around a Point at a given zoom level for a canvas of a given width and height
     * @param p The Point (in the Pyramid's projection)
     * @param z The zoom level
     * @param width The canvas width
     * @param height The canvas height
     * @return a Bounds
     */
    Bounds bounds(Point p, long z, int width, int height) {
        Grid g = grid(z)
        double boundsWidth = g.xResolution * width
        double boundsHeight = g.yResolution * height
        new Bounds(p.x - boundsWidth / 2, p.y - boundsHeight / 2, p.x + boundsWidth / 2, p.y + boundsHeight / 2, this.proj)
    }

    /**
     * Calculate the Bounds for the given Tile
     * @param t The Tile
     * @return A Bounds
     */
    Bounds bounds(Tile t) {
        Grid m = grid(t.z)
        if (m == null) {
            throw new IllegalArgumentException("No grid for zoom level ${t.z}")
        }
        int w = m.width
        int h = m.height
        double dx = bounds.width / (w as double)
        double dy = bounds.height / (h as double)

        double x
        if (origin == Origin.BOTTOM_LEFT || origin == Origin.TOP_LEFT) {
            x = bounds.minX + dx * t.x
        } else {
            x = bounds.minX + (dx * (w - t.x)) - dx
        }

        double y
        if (origin == Origin.BOTTOM_LEFT || origin == Origin.BOTTOM_RIGHT) {
            y = bounds.minY + dy * t.y
        } else {
            y = bounds.minY + (dy * (h - t.y)) - dy
        }

        new Bounds(x, y, x + dx, y + dy, this.proj)
    }

    /**
     * Get Tile coordinates (minX, minY, maxX, maxY) for the given Bounds and zoom level
     * @param b The Bounds
     * @param z The zoom level
     * @return A Map with tile coordinates (minX, minY, maxX, maxY)
     */
    Map getTileCoordinates(Bounds b, long z) {
        getTileCoordinates(b, grid(z))
    }

    /**
     * Get Tile coordinates (minX, minY, maxX, maxY) for the given Bounds and Grid
     * @param b The Bounds
     * @param g The Grid
     * @return A Map with tile coordinates (minX, minY, maxX, maxY)
     */
    Map getTileCoordinates(Bounds b, Grid g) {
        int minX = Math.floor((((b.minX - bounds.minX) / bounds.width) * g.width))
        int maxX = Math.ceil(((b.maxX - bounds.minX) / bounds.width) * g.width) - 1
        if (this.origin == Pyramid.Origin.TOP_RIGHT || this.origin == Pyramid.Origin.BOTTOM_RIGHT) {
            int invertedMinX = g.width - maxX
            int invertedMaxX = g.width - minX
            minX = invertedMinX - 1
            maxX = invertedMaxX - 1
        }
        int minY = Math.floor(((b.minY - bounds.minY) / bounds.height) * g.height)
        int maxY = Math.ceil(((b.maxY - bounds.minY) / bounds.height) * g.height) - 1
        if (this.origin == Pyramid.Origin.TOP_LEFT || this.origin == Pyramid.Origin.TOP_RIGHT) {
            int invertedMinY = g.height - maxY
            int invertedMaxY = g.height - minY
            minY = invertedMinY - 1
            maxY = invertedMaxY - 1
        }
        [minX: minX, minY: minY, maxX: maxX, maxY: maxY]
    }

    /**
     * Create a Pyramid from a String.  The String can be a well known name (GlobalMercator or GlobalMercatorBottomLeft),
     * a JSON String or File, an XML String or File, or a CSV String or File
     * @param str A Pyramid String or File
     * @return A Pyramid or null
     */
    static Pyramid fromString(String str) {
        // Well known names
        if (str.equalsIgnoreCase("GlobalMercator") || str.equalsIgnoreCase("mercator")) {
            Pyramid.createGlobalMercatorPyramid()
        } else if (str.equalsIgnoreCase("GlobalMercatorBottomLeft")) {
            Pyramid.createGlobalMercatorPyramid(origin: Pyramid.Origin.BOTTOM_LEFT)
        } else if (str.equalsIgnoreCase("GlobalMercatorTopLeft")) {
            Pyramid.createGlobalMercatorPyramid(origin: Pyramid.Origin.TOP_LEFT)
        } else if (str.equalsIgnoreCase("GlobalGeodetic") || str.equalsIgnoreCase("geodetic")) {
            Pyramid.createGlobalGeodeticPyramid()
        } else {
            File file = new File(str)
            if (file.exists()){
                str = file.text
            }
            Pyramid pyramid = null
            for (PyramidReader reader : PyramidReaders.list()) {
                try {
                    pyramid = reader.read(str)
                } catch(Exception ex){
                    // Just try the next reader
                }
            }
            pyramid
        }
    }

    /**
     * Create a Pyramid with Grids for common global web mercator tile sets.
     * @param options The optional named parameters:
     * <ul>
     *     <li>origin = The Pyramid Origin (defaults to bottom left)</li>
     *     <li>maxZoom = The max zoom level (defaults to 19)</li>
     * </ul>
     * @return A Pyramid
     */
    static Pyramid createGlobalMercatorPyramid(Map options = [:]) {
        Projection latLonProj = new Projection("EPSG:4326")
        Projection mercatorProj = new Projection("EPSG:3857")
        Bounds latLonBounds = new Bounds(-179.99, -85.0511, 179.99, 85.0511, latLonProj)
        Bounds mercatorBounds = latLonBounds.reproject(mercatorProj)
        Pyramid p = new Pyramid(
            proj: mercatorProj,
            bounds: mercatorBounds,
            origin: options.get("origin", Pyramid.Origin.BOTTOM_LEFT),
            tileWidth: 256,
            tileHeight: 256
        )
        int maxZoom = options.get("maxZoom", 19)
        p.grids = (0..maxZoom).collect { int z ->
            int n = Math.pow(2, z)
            // http://wiki.openstreetmap.org/wiki/Zoom_levels
            double res = 156412.0 / n
            new Grid(z, n, n, res, res)
        }
        p
    }

    /**
     * Create a Pyramid with Grids for common global geodetic tile sets.
     * http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic
     * @param options The optional named parameters:
     * <ul>
     *     <li>maxZoom = The max zoom level (defaults to 19)</li>
     * </ul>
     * @return A Pyramid
     */
    static Pyramid createGlobalGeodeticPyramid(Map options = [:]) {
        Projection latLonProj = new Projection("EPSG:4326")
        Bounds latLonBounds = new Bounds(-179.99, -89.99, 179.99, 89.99, latLonProj)
        Pyramid p = new Pyramid(
                proj: latLonProj,
                bounds: latLonBounds,
                origin: Pyramid.Origin.BOTTOM_LEFT,
                tileWidth: 256,
                tileHeight: 256
        )
        int maxZoom = options.get("maxZoom", 19)
        p.grids = (0..maxZoom).collect { int z ->
            int col = Math.pow(2, z + 1)
            int row = Math.pow(2, z)
            double res = 0.703125 / Math.pow(2, z)
            new Grid(z, col, row, res, res)
        }
        p
    }
}


<|start_filename|>src/test/groovy/geoscript/render/io/MapReadersTest.groovy<|end_filename|>
package geoscript.render.io

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

class MapReadersTest {

    @Test void find() {
        assertNotNull(MapReaders.find("xml"))
        assertNotNull(MapReaders.find("json"))
        assertNull(MapReaders.find("asdf"))
    }

    @Test void list() {
        List<MapReader> mapReaders = MapReaders.list()
        assertFalse(mapReaders.isEmpty())
    }

}


<|start_filename|>src/main/groovy/geoscript/layer/WMS.groovy<|end_filename|>
package geoscript.layer

import geoscript.geom.Bounds
import geoscript.proj.Projection
import org.geotools.http.HTTPClient
import org.geotools.ows.wms.Layer as GtLayer
import org.geotools.http.SimpleHttpClient
import org.geotools.ows.wms.StyleImpl
import org.geotools.ows.wms.WMSCapabilities
import org.geotools.ows.wms.WebMapServer
import javax.imageio.ImageIO
import java.awt.image.BufferedImage

/**
 * The Web Map Server (WMS) module is a connection to a remote WMS server.
 * <p><blockquote><pre>
 * def wms = new WMS("http://localhost:8080/geoserver/ows?service=wms&version=1.1.1&request=GetCapabilities")
 * </pre></blockquote></p>
 * You can use it to get information on the WMS Server just as name, version, formats, layers:
 * <p><blockquote><pre>
 * println "Name: ${wms.name}"
 * println "Version: ${wms.version}"
 * println "Formats: ${wms.getMapFormats.join(',')}"
 * wms.layers.each{layer -> println "${layer.name}" }
 * </pre></blockquote></p>
 * Or you can use it to render images or extract Rasters:
 * <p><blockquote><pre>
 * def image = wms.getImage("world:borders")
 * ImageIO.write(image,"png",new File("world.png"))
 * def raster = wms.getRaster(["world:cities","world:borders"])
 * ImageIO.write(raster.image,"png",new File("raster_world_cities.png"))
 * </pre></blockquote></p>
 * @author <NAME>
 */
class WMS {

    /**
     * The GeoTools WebMapServer
     */
    WebMapServer wms

    /**
     * A cached WMSCapabilities
     */
    private final WMSCapabilities capabilities

    /**
     * The URL
     */
    private final URL url

    /**
     * The cached List of WMS.Layers
     */
    private List<WMS.Layer> cachedLayers

    /**
     * Create a new WMS object with a get capabilities URL
     * @param options Optional named parameters:
     * <ul>
     *     <li> user = The user name for password protected urls</li>
     *     <li> password = The password for password protected urls</li>
     * </ul>
     * @param url The get capabilities URL
     */
    WMS(Map options = [:], String url) {
        this(options, new URL(url))
    }

    /**
     * Create a new WMS object with a get capabilities URL
     * @param options Optional named parameters:
     * <ul>
     *     <li> user = The user name for password protected urls</li>
     *     <li> password = The password for password protected urls</li>
     * </ul>
     * @param url The get capabilities URL
     */
    WMS(Map options = [:], URL url) {
        this.url = url
        if (options.containsKey("user") && options.containsKey("password")) {
            HTTPClient client = new SimpleHttpClient()
            client.user = options.user
            client.password = options.password
            wms = new WebMapServer(url, client)
        } else {
            wms = new WebMapServer(url)
        }
        capabilities = wms.capabilities
    }

    /**
     * The String representation
     * @return
     */
    String toString() {
        url.toString()
    }

    /**
     * Get a Raster from the WMS server
     * @param options Named parameter options may include:
     * <ul>
     *      <li>width: The width of the image (defaults to 512)</li>
     *      <li>height: The height of the image (defaults to 512)</li>
     *      <li>format: The image format (defaults to image/png)</li>
     *      <li>srs: The srs or projection id (defaults to EPSG:4326)</li>
     *      <li>transparent: Whether the image should be transparent or not (defaults to true)</li>
     *      <li>bounds: The Bounds of the image (defaults to the extent of all Layers)</li>
     * </ul>
     * @param layers The Layers to include on the image.  Layers can be a single WMS.Layer, layer name, or
     * map with name and style keys or it can be a List of the previously mentioned values.
     * @return A Raster
     */
    Raster getRaster(Map options = [:], def layers) {
        if (!(layers instanceof List)) {
            layers = [layers]
        }
        List wmsLayers = layers.collect{layer ->
            layer instanceof Layer ? layer : this.getLayer(layer instanceof String ? layer : layer.name)
        }
        Bounds bounds = options.get("bounds",null)
        if (!bounds) {
            wmsLayers.each {layer ->
                if (!bounds) {
                    bounds = layer.latLonBounds
                } else {
                    bounds.expand(layer.latLonBounds)
                }
            }
        }
        options["bounds"] = bounds
        /*WMSCoverageReader reader = new WMSCoverageReader(wms, wmsLayers[0].layer)
        (1..wmsLayers.size()).each{i ->
            reader.addLayer(wmsLayers[i].layer)
        }
        int w = options.get('width', 512)
        int h = options.get('height', 512)
        def c = options.get("backgroundColor",null)
        new Raster(reader.getMap(bounds.env, w, h, c ? new Color(c).asColor() : null))*/
        def image = getImage(options, layers)
        new Raster(image, bounds)

    }

    /**
     * Get a Image from the WMS server.
     * @param options Named parameter options may include:
     * <ul>
     *      <li>width: The width of the image (defaults to 512)</li>
     *      <li>height: The height of the image (defaults to 512)</li>
     *      <li>format: The image format (defaults to image/png)</li>
     *      <li>srs: The srs or projection id (defaults to EPSG:4326)</li>
     *      <li>transparent: Whether the image should be transparent or not (defaults to true)</li>
     *      <li>bounds: The Bounds of the image (defaults to the extent of all Layers)</li>
     *      <li>debug: Show the WMS request url</li>
     * </ul>
     * @param layers The Layers to include on the image.  Layers can be a single WMS.Layer, layer name, or
     * map with name and style keys or it can be a List of the previously mentioned values.
     * @return A BufferedImage
     */
    BufferedImage getImage(Map options = [:], def layers) {
        int w = options.get('width', 512)
        int h = options.get('height', 512)
        String format = options.get("format","image/png")
        String srs = options.get('srs', "EPSG:4326")
        boolean isTransparent = options.get("transparent", true)
        if (!(layers instanceof List)) {
            layers = [layers]
        }

        Bounds bounds = options.get("bounds",null)
        if (!bounds) {
            layers.each {layer ->
                Layer l = layer instanceof Layer ? layer : this.getLayer(layer instanceof String ? layer : layer.name)
                if (!bounds) {
                    bounds = l.getBounds(srs)
                } else {
                    bounds.expand(l.getBounds(srs))
                }
            }
        }

        def mapRequest = wms.createGetMapRequest()
        mapRequest.setDimensions(w,h)
        mapRequest.SRS = srs
        mapRequest.format = format;
        mapRequest.setBBox(new org.geotools.ows.wms.CRSEnvelope(srs, bounds.minX, bounds.minY, bounds.maxX, bounds.maxY))
        mapRequest.transparent = isTransparent
        layers.each{layer ->
            String name
            String style = ""
            if (layer instanceof Map) {
                name = layer.name
                style = layer.get("style","")
            } else {
                name = layer.toString()
            }
            mapRequest.addLayer(name, style)
        }
        if (options.get("debug", false)) {
            println mapRequest.finalURL
        }
        def response = wms.issueRequest(mapRequest)
        ImageIO.read(response.inputStream)
    }

    /**
     * Get a legend from a WMS service.
     * @param options Named parameter options
     * <ul>
     *      <li>width: The width to the image (defaults to 32)</li>
     *      <li>height: The height to the image (defaults to 32)</li>
     *      <li>format: The image format (defaults to image/png)</li>
     *      <li>exceptions: The exceptions WMS GET_LEGEND request value</li>
     *      <li>featureType: The featureType WMS GET_LEGEND request value</li>
     *      <li>rule: The rule WMS GET_LEGEND request value</li>
     *      <li>scale: The scale WMS GET_LEGEND request value</li>
     *      <li>sld: The sld WMS GET_LEGEND request value</li>
     *      <li>sldBody: The sldBody WMS GET_LEGEND request value</li>
     *      <li>style: The style WMS GET_LEGEND request value</li>
     *      <li>debug: Show the WMS request url</li>
     * </ul>
     * @return A BufferedImage
     */
    BufferedImage getLegend(Map options = [:], def layer) {
        def request = wms.createGetLegendGraphicRequest()
        // layer
        request.layer = layer.toString()
        // width
        request.width = options.get("width", 32)
        // height
        request.height = options.get("height", 32)
        // format
        request.format = options.get("format","image/png")
        // exceptions
        if (options.containsKey("exceptions")) {
            request.exceptions = options.get("exceptions")
        }
        // featureType
        if (options.containsKey("featureType")) {
            request.featureType = options.get("featureType")
        }
        // rule
        if (options.containsKey("rule")) {
            request.rule = options.get("rule")
        }
        // scale
        if (options.containsKey("scale")) {
            request.scale = options.get("scale")
        }
        // sld
        if (options.containsKey("sld")) {
            request.setSLD(options.get("sld"))
        }
        // sldBody
        if (options.containsKey("sldBody")) {
            request.setSLDBody(options.get("sldBody"))
        }
        // style
        if (options.containsKey("style")) {
            request.style = options.get("style")
        }
        if (options.get("debug", false)) {
            println request.finalURL
        }
        def response = wms.issueRequest(request)
        ImageIO.read(response.inputStream)
    }

    /**
     * Get the name (WMT_MS_Capabilities/Service/Name)
     * @return The name
     */
    String getName() {
        capabilities?.service?.name
    }

    /**
     * Get the title (WMT_MS_Capabilities/Service/Title)
     * @return The title
     */
    String getTitle() {
        capabilities?.service?.title
    }

    /**
     * Get the abstract (WMT_MS_Capabilities/Service/Abstract)
     * @return The abstract
     */
    String getAbstract() {
        capabilities?.service?.get_abstract()
    }

    /**
     * Get a List of keywords
     * @return A List of keywords
     */
    List getKeywords() {
        capabilities?.service?.keywordList.findAll{it != null && it.trim().length() > 0}
    }

    /**
     * Get the online resource URL
     * @return The online resource URL
     */
    URL getOnlineResource() {
        capabilities?.service?.onlineResource
    }

    /**
     * Get the update sequence
     * @return The update sequence
     */
    String getUpdateSequence() {
        capabilities?.updateSequence
    }

    /**
     * Get the version
     * @return The version
     */
    String getVersion() {
        capabilities?.version
    }

    /**
     * Get the GetMap formats
     * @return The GetMap formats
     */
    List getGetMapFormats() {
        capabilities?.request?.getMap?.formats
    }

    /**
     * Get a List of WMS.Layers
     * @return A List of WMS.Layers
     */
    List getLayers() {
        if (!this.cachedLayers) {
            this.cachedLayers = []
            capabilities.layerList.each{layer ->
                if (layer.name != null) {
                    this.cachedLayers.add(new Layer(layer))
                }
            }
        }
        this.cachedLayers
    }

    /**
     * Get a WMS.Layer by name
     * @param The name
     * @return A WMS.Layer or null
     */
    Layer getLayer(String name) {
        getLayers().find{layer ->
            if (name.equalsIgnoreCase(layer.name)) {
                return layer
            }
        }
    }

    /**
     * A WMS.Layer
     * @author <NAME>
     */
    protected static class Layer {

        /**
         * The wrapped GeoTools WMS Layer
         */
        GtLayer layer

        /**
         * Create a WMS.Layer from a GeoTools WMS Layer
         * @param The GeoTools WebMapServer
         * @param layer The Layer
         */
        Layer(GtLayer layer) {
            this.layer = layer
        }

        /**
         * Get the name of the layer
         * @return The name of the layer
         */
        String getName() {
            layer.name
        }

        /**
         * Get the title of the layer
         * @return The title of the layer
         */
        String getTitle() {
            layer.title
        }

        /**
         * Get the SRSes.
         * @return The SRSes
         */
        List getSrses() {
            def srses = []
            srses.addAll(layer.srs)
            srses
        }

        /**
         * Get the list of Styles
         * @return A list of Styles
         */
        List<Style> getStyles() {
            layer.styles.collect{style ->
                new Style(style)
            }
        }

        /**
         * Is the layer queryable.
         * @return Whether the layer is queryable
         */
        boolean isQueryable() {
            layer.isQueryable()
        }

        /**
         * Get the maximum scale denominator
         * @return The maximum scale denominator
         */
        double getMaxScale() {
            layer.scaleDenominatorMax
        }

        /**
         * Get the minimum scale denominator
         * @return The minimum scale denominator
         */
        double getMinScale() {
            layer.scaleDenominatorMin
        }

        /**
         * Get the parent WMS.Layer
         * @return The parent WMS.Layer
         */
        Layer getParent() {
            layer.parent != null ? new Layer(layer.parent) : null
        }

        /**
         * Get a List of child WMS.Layers
         * @return The list of child WMS.Layers
         */
        List<Layer> getChildren() {
            layer.layerChildren.collect{child ->
                new Layer(child)
            }
        }

        /**
         * Get the List of Bounds
         * @return The List of Bounds
         */
        List getBounds() {
            layer.layerBoundingBoxes.collect{bbox ->
                new Bounds(bbox.minX, bbox.minY, bbox.maxX, bbox.maxY, new Projection(bbox.coordinateReferenceSystem))
            }
        }

        /**
         * Get a Bounds in the given Projection
         * @param proj The Projection
         * @return The Bounds
         */
        Bounds getBounds(def proj) {
            def b
            def env = layer.getBoundingBoxes().get(proj instanceof Projection ? proj.id : proj)
            if (env) {
                b = new Bounds(env.minX, env.minY, env.maxX, env.maxY, proj)
            }
            if (!b) {
                b = getLatLonBounds().reproject(proj)
            }
            b
        }

        /**
         * Get the Bounds in latitude/longitude
         * @return The Bounds in latitude/longitude
         */
        Bounds getLatLonBounds() {
            def bbox = layer.getLatLonBoundingBox()
            if (bbox != null) {
                def crs = new Projection("EPSG:4326")
                try {
                    crs = new Projection(bbox.coordinateReferenceSystem)
                } catch (NullPointerException e) {}
                new Bounds(bbox.minX, bbox.minY, bbox.maxX, bbox.maxY, crs)
            } else {
                null
            }
        }

        /**
         * The string representation
         * @return The string representation
         */
        String toString() {
            getName()
        }
    }

    /**
     * A WMS Style.
     * @author <NAME>
     */
    private static class Style {

        /**
         * The wrapped GeoTools WMS Style
         */
        StyleImpl style

        /**
         * Create a Style from a GeoTools StyleImpl
         * @param style The GeoTools StyleImpl
         */
        Style(StyleImpl style) {
            this.style = style
        }

        /**
         * Get the name
         * @return The name
         */
        String getName() {
            style.name
        }

        /**
         * Get the title
         * @return The title
         */
        String getTitle() {
            style.title
        }

        /**
         * Get the abstract
         * @return The abstract
         */
        String getAbstract() {
            style.abstract.toString()
        }

        /**
         * The string representation
         * @return The string representation
         */
        String toString() {
            name
        }
    }
}

<|start_filename|>Dockerfile<|end_filename|>
FROM groovy:jre8
COPY target/geoscript-groovy-1.18.0-app/geoscript-groovy-1.18.0 /usr/src/geoscript-groovy
WORKDIR /usr/src/geoscript-groovy
CMD ["bin/geoscript-groovysh"]

<|start_filename|>src/main/groovy/geoscript/layer/TMS.groovy<|end_filename|>
package geoscript.layer

/**
 * A Tile Map Service (TMS) TileLayer.  A TMS TileLayer can read or write Tiles
 * if given a File directory but it is ready only if given a URL.
 * @author <NAME>
 */
class TMS extends ImageTileLayer {

    /**
     * The File directory
     */
    File dir

    /**
     * The base URL
     */
    URL url

    /**
     * The Pyramid structure
     */
    Pyramid pyramid

    /**
     * The image type (png, jpeg, gif)
     */
    String imageType

    /**
     * Create a TMS TileLayer for a File directory
     * @param name The name of the TMS TileLayer
     * @param imageType The image type (png, jpeg, gif)
     * @param fileOrUrl The File directory or URL
     * @param pyramid The Pyramid
     */
    TMS(String name, String imageType, String fileOrUrl, Pyramid pyramid) {
        this.name = name
        this.imageType = imageType
        if (fileOrUrl.startsWith("http")) {
            this.url = new URL(fileOrUrl)
        } else {
            this.dir = new File(fileOrUrl)
        }
        this.pyramid = pyramid
        this.bounds = pyramid.bounds
        this.proj = pyramid.proj
    }

    /**
     * Create a TMS TileLayer for a File directory
     * @param name The name of the TMS TileLayer
     * @param imageType The image type (png, jpeg, gif)
     * @param dir The File directory
     * @param pyramid The Pyramid
     */
    TMS(String name, String imageType, File dir, Pyramid pyramid) {
        this.name = name
        this.imageType = imageType
        this.dir = dir
        this.pyramid = pyramid
        this.bounds = pyramid.bounds
        this.proj = pyramid.proj
    }

    /**
     * Create a TMS TileLayer with a base URL
     * @param name The name of the TMS TileLayer
     * @param imageType The image type (png, jpeg, gif)
     * @param url The base URL
     * @param pyramid The Pyramid
     */
    TMS(String name, String imageType, URL url, Pyramid pyramid) {
        this.name = name
        this.imageType = imageType
        this.url = url
        this.pyramid = pyramid
        this.bounds = pyramid.bounds
        this.proj = pyramid.proj
    }

    /**
     * Get a Tile
     * @param z The zoom level
     * @param x The column
     * @param y The row
     * @return A Tile
     */
    @Override
    ImageTile get(long z, long x, long y) {
        ImageTile tile = new ImageTile(z, x, y)
        if (dir) {
            File file = new File(new File(new File(this.dir, String.valueOf(z)), String.valueOf(x)), "${y}.${imageType}")
            if (file.exists()) {
                tile.data = file.bytes
            }
        } else {
            String urlString = url.toString()
            URL tileUrl = new URL("${urlString}${urlString.endsWith("/") ? '' : '/'}${z}/${x}/${y}.${imageType}")
            tileUrl.withInputStream {input ->
                tile.data = input.bytes
            }
        }
        tile
    }

    /**
     * Add a Tile
     * @param t The Tile
     */
    @Override
    void put(ImageTile t) {
        if(!dir) {
            throw new IllegalArgumentException("TMS with URL are ready only!")
        }
        File file = new File(new File(new File(this.dir, String.valueOf(t.z)), String.valueOf(t.x)), "${t.y}.${imageType}")
        if (!file.parentFile.exists()) {
            file.parentFile.mkdirs()
        }
        file.withOutputStream {out ->
            out.write(t.data)
        }
    }

    /**
     * Delete a Tile
     * @param t The Tile
     */
    @Override
    void delete(ImageTile t) {
        if(!dir) {
            throw new IllegalArgumentException("TMS with URL are ready only!")
        }
        File file = new File(new File(new File(this.dir, String.valueOf(t.z)), String.valueOf(t.x)), "${t.y}.${imageType}")
        if (file.exists()) {
            file.delete()
        }
    }

    /**
     * Close the TileLayer
     */
    @Override
    void close() throws IOException {
        // Do nothing
    }

    /**
     * The TMS TileLayerFactory
     */
    static class Factory extends TileLayerFactory<TMS> {

        @Override
        TMS create(Map params) {
            String type = params.get("type","").toString()
            if (type.equalsIgnoreCase("tms")) {
                Object fileOrUrl = params.get("file", params.get("url"))
                String name = params.get("name", fileOrUrl instanceof File ? (fileOrUrl as File).name : "tms")
                String imageType = params.get("format", "png")
                Object p = params.get("pyramid", Pyramid.createGlobalMercatorPyramid())
                Pyramid pyramid = p instanceof Pyramid ? p as Pyramid : Pyramid.fromString(p as String)
                new TMS(name, imageType, fileOrUrl, pyramid)
            } else {
                null
            }
        }

        @Override
        TileRenderer getTileRenderer(Map options, TileLayer tileLayer, List<Layer> layers) {
            if (tileLayer instanceof TMS) {
                new ImageTileRenderer(tileLayer, layers)
            } else {
                null
            }
        }
    }
}


<|start_filename|>src/main/groovy/geoscript/workspace/Directory.groovy<|end_filename|>
package geoscript.workspace

import geoscript.GeoScript
import geoscript.layer.Layer
import org.geotools.data.DataStore
import org.geotools.data.directory.DirectoryDataStore
import org.geotools.data.shapefile.ShapefileDataStore
import org.geotools.data.shapefile.ShapefileDataStoreFactory
import org.geotools.data.shapefile.files.ShpFileType
import org.geotools.util.URLs

/**
 * A Directory Workspace can contain one or more Shapefiles.
 * <p><blockquote><pre>
 * Directory dir = new Directory("shapefiles")
 * Layer layer = dir.get("states")
 * </pre></blockquote></p>
 * @author <NAME>
 */
class Directory extends Workspace {

    /**
     * Create a Directory Workspace from a File directory
     * @param dir The File directory
     */
    Directory(File dir) {
        super(new DirectoryDataStore(dir, new ShapefileDataStoreFactory.ShpFileStoreFactory(new ShapefileDataStoreFactory(),[:])));
    }

    /**
     * Create a Directory Workspace from a File directory
     * @param dir The File directory as a String
     */
    Directory(String dir) {
        this(new File(dir))
    }

    /**
     * Create a Directory Workspace from a GeoTools DirectoryDataStore
     * @param ds The GeoTools DirectoryDataStore
     */
    Directory(DirectoryDataStore ds) {
        super(ds)
    }

    /**
     * Create a Directory Workspace from a GeoTools ShapefileDataStore
     * @param ds The GeoTools ShapefileDataStore
     */
    Directory(ShapefileDataStore ds) {
        super(ds)
    }

    /**
     * Get the format
     * @return The workspace format name
     */
    String getFormat() {
        return "Directory"
    }

    /**
     * Get a Layer by name
     * @param The Layer name
     * @return A Layer
     */
    @Override
    Layer get(String name) {
        if (name.endsWith(".shp")) {
            super.get(name.substring(0,name.lastIndexOf(".shp")))
        } else {
            super.get(name)
        }
    }

    /**
     * Get the File or Directory
     * @return The File or Directory
     */
    File getFile() {
        if (ds instanceof ShapefileDataStore) {
            new File(ds.shpFiles.get(ShpFileType.SHP))
        } else {
            new File(ds.info.source.path)
        }
    }

    /**
     * Remove a Layer by name from this Workspace
     * @param name The Layer name
     */
    @Override
    void remove(String name) {
        ds.removeSchema(name)
        ds.cache.refreshCacheContents()
    }

    /**
     * The string representation
     * @return The string representation
     */
    String toString() {
        "Directory[${getFile().absolutePath}]"
    }

    /**
     * Get a Directory from a zipped Shapefile
     * @param options Optional named parameters:
     * <ul>
     *     <li>overwrite = Whether to overwrite the existing file or not (defaults to true) </li>
     * </ul>
     * @param url The URL of the zipped Shapefile
     * @param dir The File directory where we will unzip the zip file
     * @return A Directory Workspace
     */
    static Directory fromURL(Map options = [:], URL url, File dir) {
        if (!dir.exists()) {
            dir.mkdir()
        }
        File file = File.createTempFile("download",".zip")
        GeoScript.download(url, file, overwrite: options.get("overwrite", true) as boolean)
        GeoScript.unzip(file, dir)
        new Directory(dir)
    }

    /**
     * The Directory WorkspaceFactory
     */
    static class Factory extends WorkspaceFactory<Directory> {

        @Override
        Map getParametersFromString(String str) {
            Map params = [:]
            if (!str.contains("=") && str.endsWith(".shp")) {
                if (str.startsWith("file:/")) {
                    params.put("type","shapefile")
                    params.put("file", URLs.urlToFile(new URL(str)).getAbsoluteFile().getParentFile())
                } else {
                    params.put("type","shapefile")
                    params.put("file",new File(str).getAbsoluteFile().getParentFile())
                }
            } else if (!str.contains("=") && new File(str).isDirectory()) {
                params.put("type","shapefile")
                params.put("file", new File(str))
            } else {
                params = super.getParametersFromString(str)
            }
            params
        }

        @Override
        Directory create(String type, Map params) {
            if (type.equalsIgnoreCase('shapefile') && params.containsKey('file')) {
                File file = params.get('file') instanceof File ? params.get('file') : new File(params.get('file'))
                if (!file.isDirectory()) {
                    file = file.parentFile
                }
                new Directory(file)
            } else if (type.equalsIgnoreCase('shapefile') && params.containsKey('url') && params.containsKey("dir")) {
                URL url = params.get('url') instanceof URL ? params.get('url') : new URL(params.get('url'))
                File dir = params.get('dir') instanceof File ? params.get('dir') : new File(params.get('dir'))
                if (!dir.exists()) {
                    dir.mkdir()
                }
                File file = File.createTempFile("download",".zip")
                GeoScript.download(url, file, overwrite: params.get("overwrite", true) as boolean)
                GeoScript.unzip(file, dir)
                new Directory(dir)
            } else {
                null
            }
        }

        @Override
        Directory create(DataStore dataStore) {
            if (dataStore != null && (
                    dataStore instanceof org.geotools.data.directory.DirectoryDataStore ||
                    dataStore instanceof org.geotools.data.shapefile.ShapefileDataStore)) {
                new Directory(dataStore)
            } else {
                null
            }
        }
    }

}

<|start_filename|>src/main/groovy/geoscript/carto/ImageItem.groovy<|end_filename|>
package geoscript.carto

/**
 * Add an image to a cartographic document
 * @author <NAME>
 */
class ImageItem extends Item {

    /**
     * The File or URL
     */
    Object path

    /**
     * Create an Image from the top left
     * @param x The number of pixels from the left
     * @param y The number of pixels from the top
     * @param width The width in pixels
     * @param height The height in pixels
     */
    ImageItem(int x, int y, int width, int height) {
        super(x, y, width, height)
    }

    /**
     * Load an Image from a file
     * @param file The image file
     * @return The ImageItem
     */
    ImageItem path(File file) {
        this.path = file
        this
    }

    @Override
    String toString() {
        "ImageItem(x = ${x}, y = ${y}, width = ${width}, height = ${height}, path = ${path})"
    }

}


<|start_filename|>src/test/groovy/geoscript/feature/FieldTest.groovy<|end_filename|>
package geoscript.feature

import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*
import geoscript.proj.Projection

/**
 * The Field Unit Test
 */
class FieldTest {

    @Test void constructors() {

        Field f1 = new Field("name","String")
        assertEquals "name: String", f1.toString()
        assertEquals "name", f1.name
        assertEquals "String", f1.typ

        Field f2 = new Field("geom","Point", "EPSG:2927")
        assertEquals "geom: Point(EPSG:2927)", f2.toString()
        assertEquals "geom", f2.name
        assertEquals "Point", f2.typ
        assertEquals "EPSG:2927", f2.proj.toString()

        Field f3 = new Field("geom","Point", new Projection("EPSG:2927"))
        assertEquals "geom: Point(EPSG:2927)", f3.toString()
        assertEquals "geom", f3.name
        assertEquals "Point", f3.typ
        assertEquals "EPSG:2927", f3.proj.toString()

        // Create a Field from a List of Parts
        Field f4 = new Field(["name","String"])
        assertEquals "name: String", f4.toString()
        assertEquals "name", f4.name
        assertEquals "String", f4.typ

        Field f5 = new Field(["geom","Point", new Projection("EPSG:2927")])
        assertEquals "geom: Point(EPSG:2927)", f5.toString()
        assertEquals "geom", f5.name
        assertEquals "Point", f5.typ
        assertEquals "EPSG:2927", f5.proj.toString()

        // Create a Field from a Map
        Field f6 = new Field(["name": "name", "type": "String"])
        assertEquals "name: String", f6.toString()
        assertEquals "name", f6.name
        assertEquals "String", f6.typ

        Field f7 = new Field(["name": "geom", "type": "Point", "proj": new Projection("EPSG:2927")])
        assertEquals "geom: Point(EPSG:2927)", f7.toString()
        assertEquals "geom", f7.name
        assertEquals "Point", f7.typ
        assertEquals "EPSG:2927", f7.proj.toString()

        // Create a Field from an existing Field
        Field f8 = new Field("the_geom","Point")
        Field f9 = new Field(f8)
        assertNotSame f8, f9
        assertEquals "the_geom: Point", f9.toString()

        Field f10 = new Field("the_geom","Point","EPSG:4326")
        Field f11 = new Field(f10)
        assertNotSame f10, f11
        assertEquals "the_geom: Point(EPSG:4326)", f11.toString()
    }

    @Test void isGeometry() {
        assertFalse new Field("name","String").isGeometry()
        assertTrue new Field("geom","Point", "EPSG:2927").isGeometry()
        assertTrue new Field("geom","Point", new Projection("EPSG:2927")).isGeometry()
        assertTrue new Field("geom","Geometry", new Projection("EPSG:2927")).isGeometry()
        assertTrue new Field("geom","CircularRing", new Projection("EPSG:2927")).isGeometry()
        assertTrue new Field("geom","CircularString", new Projection("EPSG:2927")).isGeometry()
        assertTrue new Field("geom","CompoundCurve", new Projection("EPSG:2927")).isGeometry()
        assertTrue new Field("geom","CompoundRing", new Projection("EPSG:2927")).isGeometry()
    }

    @Test void equalsAndHashCode() {
        // Same name and type
        assertTrue new Field("name","String").equals(new Field("name","String"))
        // Same name, type, and projection
        assertTrue new Field("geom","Point","EPSG:2927").equals(new Field("geom","Point","EPSG:2927"))
        // Different name, same type
        assertFalse new Field("name","String").equals(new Field("description","String"))
        // Same name, different type
        assertFalse new Field("name","String").equals(new Field("name","int"))
        // Same name and type, different projection
        assertFalse new Field("geom","Point","EPSG:2927").equals(new Field("geom","Point","EPSG:4326"))
    }

}



<|start_filename|>src/test/groovy/geoscript/layer/io/MvtReaderTest.groovy<|end_filename|>
package geoscript.layer.io

import geoscript.layer.Layer
import org.apache.commons.codec.binary.Base64
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals

/**
 * The MvtReader Unit Test
 * @author <NAME>
 */
class MvtReaderTest {

    @Test void readFromString() {
        URL url = getClass().getClassLoader().getResource("1582.mvt")
        Reader reader = new MvtReader()
        byte[] bytes = url.bytes
        String str = new String(Base64.encodeBase64(bytes), "UTF-8")
        Layer layer = reader.read(str)
        assertEquals 6, layer.count
    }

    @Test void readFromUrl() {
        URL url = getClass().getClassLoader().getResource("1582.mvt")
        MvtReader reader = new MvtReader()
        Layer layer = reader.read(url)
        assertEquals 6, layer.count
    }

    @Test void readFromFile() {
        URL url = getClass().getClassLoader().getResource("1582.mvt")
        Reader reader = new MvtReader()
        Layer layer = reader.read(new File(url.toURI()))
        assertEquals 6, layer.count
    }

    @Test void readFromInputStream() {
        URL url = getClass().getClassLoader().getResource("1582.mvt")
        InputStream input = url.openStream()
        Reader reader = new MvtReader()
        Layer layer = reader.read(input)
        input.close()
        assertEquals 6, layer.count
    }

}


<|start_filename|>src/test/groovy/geoscript/viewer/ViewerTest.groovy<|end_filename|>
package geoscript.viewer

import org.geotools.image.test.ImageAssert
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*
import geoscript.geom.Geometry
import javax.imageio.ImageIO

/**
 * The Viewer UnitTest
 * @author <NAME>
 */
class ViewerTest {

    @TempDir
    File folder

    @Test void drawToImage() {
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        def image = Viewer.drawToImage(geom)
        assertNotNull(image)
        def file = new File(folder,"viewer_drawtoimage.png")
        ImageIO.write(image,"png", file)
        assertTrue file.exists()
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/drawtoimage.png").toURI())
        ImageAssert.assertEquals(expectedFile, image, 100)
    }

    @Test void drawToImageWithOptions() {
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        def image = Viewer.drawToImage(geom, size: [400,400], bounds: geom.bounds.scale(2.0),
            strokeColor: "navy", fillColor: "wheat", markerShape: "cross", markerSize: 10,
            opacity: 0.65, strokeWidth: 1.5, drawCoords: true
        )
        assertNotNull(image)
        def file = new File(folder,"viewer_drawtoimage_options.png")
        ImageIO.write(image,"png", file)
        assertTrue file.exists()
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/drawtoimagewithoptions.png").toURI())
        ImageAssert.assertEquals(expectedFile, image, 100)
    }

    @Test void drawToBase64EncodedString() {
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        String str = Viewer.drawToBase64EncodedString(geom)
        assertNotNull(str)
        assertTrue(str.startsWith("image/png;base64,"))
    }

    @Test void drawToFile() {
        def file = new File(folder,"viewer_drawtofile.png")
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        Viewer.drawToFile(geom, file, size: [400,400])
        assertTrue(file.exists())
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/drawtofile.png").toURI())
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void drawToFileWithOptions() {
        def file = new File(folder,"viewer_drawtofile_options.png")
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        Viewer.drawToFile([geom.buffer(10), geom], file, size: [400,400], bounds: geom.bounds.scale(2.0),
            strokeColor: "navy", fillColor: "wheat", markerShape: "cross", markerSize: 10,
            opacity: 0.65, strokeWidth: 1.5, drawCoords: true
        )
        assertTrue file.exists()
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/drawtofilewithoptions.png").toURI())
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 100)
    }

    @Test void plotToImage() {
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        def image = Viewer.plotToImage(geom)
        assertNotNull(image)
        def file = new File(folder,"viewer_plottoimage.png")
        ImageIO.write(image,"png", file)
        assertTrue file.exists()
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/plottoimage.png").toURI())
        ImageAssert.assertEquals(expectedFile, image, 10000)
    }

    @Test void plotToImageWithOptions() {
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        def image = Viewer.plotToImage(geom, size: [400,400], legend: true, fillCoords: true, fillPolys: true)
        assertNotNull(image)
        def file = new File(folder,"viewer_plottoimage_withoptions.png")
        ImageIO.write(image,"png", file)
        assertTrue file.exists()
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/plottoimagewithoptions.png").toURI())
        ImageAssert.assertEquals(expectedFile, image, 10000)
    }

    @Test void plotToFile() {
        def file = new File(folder,"viewer_plottofile.png")
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        Viewer.plotToFile(geom, size: [400,400], file, legend: false)
        assertTrue(file.exists())
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/plottofile.png").toURI())
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 10000)
    }

    @Test void plotToFileWithOptions() {
        def file = new File(folder,"viewer_plottofile_withoptions.png")
        def geom = Geometry.fromWKT("POINT (-111 45.7)").buffer(10)
        Viewer.plotToFile([geom, geom.buffer(10)], size: [400,400], file, legend: false, drawCoords: false)
        assertTrue(file.exists())
        File expectedFile = new File(getClass().getClassLoader().getResource("geoscript/viewer/plottofilewithoptions.png").toURI())
        ImageAssert.assertEquals(expectedFile, ImageIO.read(file), 10000)
    }

}



<|start_filename|>src/test/groovy/geoscript/layer/VectorTilesTest.groovy<|end_filename|>
package geoscript.layer

import geoscript.FileUtil
import geoscript.ServerTestUtil
import geoscript.layer.io.GeoJSONWriter
import geoscript.proj.Projection
import okhttp3.HttpUrl
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir

import static org.junit.jupiter.api.Assertions.*

/**
 * The VectorTiles Unit Test
 * @author <NAME>
 */
class VectorTilesTest {

    @TempDir
    File folder

    @Test
    void getGeoJsonTilesFromUrl() {
        ServerTestUtil.withServer { MockWebServer server ->
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("tile.json"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("tile.json"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("tile.json"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("tile.json"))))
            server.start()
            HttpUrl url = server.url("")

            Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
            pyramid.origin = Pyramid.Origin.TOP_LEFT
            VectorTiles vectorTiles = new VectorTiles(
                    "States",
                    url.url(),
                    pyramid,
                    "json",
                    proj: new Projection("EPSG:4326")
            )
            List<Layer> layers = vectorTiles.getLayers(new TileCursor<Tile>(vectorTiles, 1))
            assertEquals(1, layers.size())

            assertEquals("/1/0/0.json", server.takeRequest().getPath())
            assertEquals("/1/1/0.json", server.takeRequest().getPath())
            assertEquals("/1/0/1.json", server.takeRequest().getPath())
            assertEquals("/1/1/1.json", server.takeRequest().getPath())
        }
    }

    @Test
    void getPbfTilesFromUrl() {
        ServerTestUtil.withServer { MockWebServer server ->
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("pbf/1/0/0.pbf"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("pbf/1/1/0.pbf"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("pbf/1/0/1.pbf"))))
            server.enqueue(new MockResponse().setBody(ServerTestUtil.fileToBytes(ServerTestUtil.getResource("pbf/1/1/1.pbf"))))
            server.start()
            HttpUrl url = server.url("")

            Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
            pyramid.origin = Pyramid.Origin.TOP_LEFT
            VectorTiles vectorTiles = new VectorTiles(
                    "World",
                    url.url(),
                    pyramid,
                    "pbf"
            )
            List<Layer> layers = vectorTiles.getLayers(new TileCursor<Tile>(vectorTiles, 1))
            assertEquals(2, layers.size())

            assertEquals("/1/0/0.pbf", server.takeRequest().getPath())
            assertEquals("/1/1/0.pbf", server.takeRequest().getPath())
            assertEquals("/1/0/1.pbf", server.takeRequest().getPath())
            assertEquals("/1/1/1.pbf", server.takeRequest().getPath())
        }
    }

    @Test
    void generateDirectoryOfPbfTiles() {
        // Setup
        File dir = FileUtil.createDir(folder, "states")
        dir.mkdir()
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        pyramid.origin = Pyramid.Origin.TOP_LEFT
        VectorTiles vectorTiles = new VectorTiles(
                "States",
                dir,
                pyramid,
                "pbf",
                proj: new Projection("EPSG:4326")
        )
        // Generate
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        PbfVectorTileRenderer renderer = new PbfVectorTileRenderer(shp, [shp.schema.get("STATE_NAME")])
        TileGenerator generator = new TileGenerator()
        generator.generate(vectorTiles, renderer, 0, 2)
        // Check tiles
        [
                new File(dir, "0/0/0.pbf"),
                new File(dir, "1/0/0.pbf"),
                new File(dir, "1/1/0.pbf"),
                new File(dir, "1/1/1.pbf"),
                new File(dir, "1/0/1.pbf"),
                new File(dir, "2/0/0.pbf"),
                new File(dir, "2/0/1.pbf"),
                new File(dir, "2/0/2.pbf"),
                new File(dir, "2/0/3.pbf"),
                new File(dir, "2/1/0.pbf"),
                new File(dir, "2/1/1.pbf"),
                new File(dir, "2/1/2.pbf"),
                new File(dir, "2/1/3.pbf"),
                new File(dir, "2/2/0.pbf"),
                new File(dir, "2/2/1.pbf"),
                new File(dir, "2/2/2.pbf"),
                new File(dir, "2/2/3.pbf"),
                new File(dir, "2/3/0.pbf"),
                new File(dir, "2/3/1.pbf"),
                new File(dir, "2/3/2.pbf"),
                new File(dir, "2/3/3.pbf")
        ].each { File f ->
            assertTrue f.exists()
        }
        // Read
        (0..2).each { int z ->
            vectorTiles.tiles(z).each { Tile t ->
                assertEquals z, t.z
                assertTrue t.x in [0l, 1l, 2l, 3l]
                assertTrue t.y in [0l, 1l, 2l, 3l]
                assertNotNull t.data
            }
        }
        // Delete
        Tile tile = vectorTiles.get(0,0,0)
        assertNotNull tile.data
        vectorTiles.delete(tile)
        tile = vectorTiles.get(0,0,0)
        assertNull tile.data
        // Get Layers
        List layers = vectorTiles.getLayers(vectorTiles.tiles(2, 0, 1, 1, 1))
        assertEquals 1, layers.size()
        assertTrue layers[0].count > 0
    }

    @Test
    void generateDirectoryOfGeoJsonTiles() {
        // Setup
        File dir = FileUtil.createDir(folder, "states")
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        pyramid.origin = Pyramid.Origin.TOP_LEFT
        VectorTiles vectorTiles = new VectorTiles(
                "States",
                dir,
                pyramid,
                "json",
                proj: new Projection("EPSG:4326")
        )
        // Generate
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        VectorTileRenderer renderer = new VectorTileRenderer(new GeoJSONWriter(), shp, [shp.schema.get("STATE_NAME")])
        TileGenerator generator = new TileGenerator()
        generator.generate(vectorTiles, renderer, 0, 2)
        // Check tiles
        [
                new File(dir, "0/0/0.json"),
                new File(dir, "1/0/0.json"),
                new File(dir, "1/1/0.json"),
                new File(dir, "1/1/1.json"),
                new File(dir, "1/0/1.json"),
                new File(dir, "2/0/0.json"),
                new File(dir, "2/0/1.json"),
                new File(dir, "2/0/2.json"),
                new File(dir, "2/0/3.json"),
                new File(dir, "2/1/0.json"),
                new File(dir, "2/1/1.json"),
                new File(dir, "2/1/2.json"),
                new File(dir, "2/1/3.json"),
                new File(dir, "2/2/0.json"),
                new File(dir, "2/2/1.json"),
                new File(dir, "2/2/2.json"),
                new File(dir, "2/2/3.json"),
                new File(dir, "2/3/0.json"),
                new File(dir, "2/3/1.json"),
                new File(dir, "2/3/2.json"),
                new File(dir, "2/3/3.json")
        ].each { File f ->
            assertTrue f.exists()
        }
        // Read
        (0..2).each { int z ->
            vectorTiles.tiles(z).each { Tile t ->
                assertEquals z, t.z
                assertTrue t.x in [0l, 1l, 2l, 3l]
                assertTrue t.y in [0l, 1l, 2l, 3l]
                assertNotNull t.data
            }
        }
        // Delete
        Tile tile = vectorTiles.get(0,0,0)
        assertNotNull tile.data
        vectorTiles.delete(tile)
        tile = vectorTiles.get(0,0,0)
        assertNull tile.data
        // Get Layers
        List layers = vectorTiles.getLayers(vectorTiles.tiles(2, 0, 1, 1, 1))
        assertEquals 1, layers.size()
        assertTrue layers[0].count > 0
    }

    @Test
    void generateMBTileFileOfPbfTiles() {
        // Setup
        File file = new File(folder,"states.mbtiles")
        Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
        pyramid.origin = Pyramid.Origin.TOP_LEFT
        VectorTiles vectorTiles = new VectorTiles(
                "States",
                file,
                pyramid,
                "pbf"
        )
        // Generate
        Shapefile shp = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        PbfVectorTileRenderer renderer = new PbfVectorTileRenderer(shp, [shp.schema.get("STATE_NAME")])
        TileGenerator generator = new TileGenerator()
        generator.generate(vectorTiles, renderer, 0, 2)
        // Read
        (0..2).each { int z ->
            vectorTiles.tiles(z).each { Tile t ->
                assertEquals z, t.z
                assertTrue t.x in [0l, 1l, 2l, 3l]
                assertTrue t.y in [0l, 1l, 2l, 3l]
                assertNotNull t.data
            }
        }
        // Delete
        Tile tile = vectorTiles.get(0,0,0)
        assertNotNull tile.data
        vectorTiles.delete(tile)
        tile = vectorTiles.get(0,0,0)
        assertNull tile.data
        // Get Layers
        List layers = vectorTiles.getLayers(vectorTiles.tiles(2, 0, 1, 1, 1))
        assertEquals 1, layers.size()
        assertTrue layers[0].count > 0
    }

}


<|start_filename|>src/main/groovy/geoscript/style/StyleRepository.groovy<|end_filename|>
package geoscript.style

/**
 * A StyleRepository is a way to manage styles for layers.
 * @author <NAME>
 */
interface StyleRepository {

    /**
     * Get the default style for a Layer by name
     * @param layerName The layer name
     * @return The default style
     */
    String getDefaultForLayer(String layerName)

    /**
     * Get a style by Layer name and Style name
     * @param layerName The Layer name
     * @param styleName The Style name
     * @return The Style
     */
    String getForLayer(String layerName, String styleName)

    /**
     * Get all of the styles for a Layer Name.
     * @param layerName The Layer name
     * @return A List of style maps with layerName, styleName, and style keys.
     */
    List<Map<String, String>> getForLayer(String layerName)

    /**
     * Get all styles
     * @return A List of style maps with layerName, styleName, and style keys.
     */
    List<Map<String, String>> getAll()

    /**
     * Save a style for a Layer with a given name
     * @param layerName The layer name
     * @param styleName The style name
     * @param style The style
     * @param options A Map of options
     */
    void save(String layerName, String styleName, String style, Map options)

    /**
     * Delete the given style by layer and style name
     * @param layerName The layer name
     * @param styleName The style name
     */
    void delete(String layerName, String styleName)

}

<|start_filename|>src/main/groovy/geoscript/carto/PdfCartoFactory.groovy<|end_filename|>
package geoscript.carto

/**
 * A CartoFactory that creates a CartoBuilder that produces PDF documents.
 * @author <NAME>
 */
class PdfCartoFactory implements CartoFactory {

    @Override
    String getName() {
        "pdf"
    }

    @Override
    String getMimeType() {
        "application/pdf"
    }

    @Override
    CartoBuilder create(PageSize pageSize) {
        new PdfCartoBuilder(pageSize)
    }

}


<|start_filename|>src/test/groovy/geoscript/carto/OverviewMapItemTest.groovy<|end_filename|>
package geoscript.carto

import geoscript.layer.GeoTIFF
import geoscript.layer.Shapefile
import geoscript.render.Map
import geoscript.style.Fill
import org.junit.jupiter.api.Test

import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue

class OverviewMapItemTest {

    @Test
    void create() {

        Map map = new Map(layers: [
                new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        ])

        Map overViewMap = new Map(layers: [
                new GeoTIFF(new File(getClass().getClassLoader().getResource("raster.tif").toURI())).read()
        ])

        OverviewMapItem item = new OverviewMapItem(10,20,300,400)
                .linkedMap(map)
                .overviewMap(overViewMap)
                .areaStyle(new Fill("red",0.1) + new geoscript.style.Stroke("red",1))
                .zoomIntoBounds(true)
                .scaleFactor(3.0)

        assertEquals(map, item.linkedMap)
        assertEquals(overViewMap, item.overviewMap)
        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(400, item.height)
        assertTrue(item.toString().startsWith("OverviewMapItem(x = 10, y = 20, width = 300, height = 400,"))
        assertTrue(item.toString().endsWith(")"))
    }


}


<|start_filename|>src/test/groovy/geoscript/feature/io/GeobufWriterTest.groovy<|end_filename|>
package geoscript.feature.io

import geoscript.feature.Feature
import geoscript.feature.Field
import geoscript.feature.Schema
import geoscript.geom.Point
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.*

/**
 * The GeobufWriter Unit Test
 * @author <NAME>
 */
class GeobufWriterTest {

    @Test void write() {
        GeobufWriter writer = new GeobufWriter()
        GeobufReader reader = new GeobufReader()
        Schema schema = new Schema("houses", [new Field("geom","Point"), new Field("name","string"), new Field("price","float")])
        Feature feature = new Feature([new Point(111,-47), "House", 12.5], "house1", schema)
        String hex = writer.write(feature)
        assertEquals "0a046e616d650a057072696365100218062a2d0a0c08001a0880e7ed69ffa6e92c5a06686f757365316a070a05486f7573656a060a0431322e35720400000101", hex
        Feature decodedFeature = reader.read(hex)
        assertEquals feature.geom, decodedFeature.geom
        assertEquals feature["name"], decodedFeature["name"]
        assertEquals String.valueOf(feature["price"]), decodedFeature["price"]
    }
}


<|start_filename|>src/test/groovy/geoscript/carto/ScaleBarItemTest.groovy<|end_filename|>
package geoscript.carto

import geoscript.layer.Layer
import geoscript.layer.Shapefile
import geoscript.render.Map
import org.junit.jupiter.api.Test

import java.awt.*

import static org.junit.jupiter.api.Assertions.assertEquals

class ScaleBarItemTest {

    @Test
    void create() {

        Map map = new Map(layers: [
            new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        ])

        ScaleBarItem item = new ScaleBarItem(10,20,300,50)
            .font(new Font("Verdana", Font.BOLD, 14))
            .strokeColor(Color.BLUE)
            .fillColor(Color.GRAY)
            .strokeWidth(1.45f)
            .border(6)
            .units(ScaleBarItem.Units.US)
            .map(map)

        assertEquals(map, item.map)
        assertEquals(10, item.x)
        assertEquals(20, item.y)
        assertEquals(300, item.width)
        assertEquals(50, item.height)
        assertEquals(item.font.name, "Verdana")
        assertEquals(item.font.style, Font.BOLD)
        assertEquals(item.font.size, 14)
        assertEquals(item.strokeColor, Color.BLUE)
        assertEquals(item.fillColor, Color.GRAY)
        assertEquals(item.border, 6)
        assertEquals(item.strokeWidth, 1.45f, 0.01f)
        assertEquals(ScaleBarItem.Units.US, item.units)
    }

    @Test void calculateScaleBarInfoMeters() {

        Layer layer = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        Map map = new Map(
            bounds: layer.bounds,
            proj: layer.proj,
            width: 400,
            height: 400,
            layers: [ layer ]
        )
        ScaleBarItem item = new ScaleBarItem(10,20,300,50)
                .units(ScaleBarItem.Units.METRIC)
                .map(map)
        ScaleBarItem.ScaleBarInfo info = item.calculateScaleBarInfo()
        assertEquals(311.0, info.widthInPixels, 0.01)
        assertEquals(5000, info.widthInUnits, 0.01)
        assertEquals("km", info.unitForScaleText)
    }

    @Test void calculateScaleBarInfoMiles() {

        Layer layer = new Shapefile(new File(getClass().getClassLoader().getResource("states.shp").toURI()))
        Map map = new Map(
                bounds: layer.bounds,
                proj: layer.proj,
                width: 400,
                height: 400,
                layers: [ layer ]
        )
        ScaleBarItem item = new ScaleBarItem(10,20,300,50)
                .units(ScaleBarItem.Units.US)
                .map(map)
        ScaleBarItem.ScaleBarInfo info = item.calculateScaleBarInfo()
        assertEquals(300.0, info.widthInPixels, 0.01)
        assertEquals(3000.0, info.widthInUnits, 0.01)
        assertEquals("miles", info.unitForScaleText)
    }


}


<|start_filename|>src/main/groovy/geoscript/style/DirectoryStyleRepository.groovy<|end_filename|>
package geoscript.style

import org.apache.commons.io.FilenameUtils

/**
 * A StyleRepository that uses a flat directory structure.
 * Each file in the given directory is the default style for the layer.
 * @author <NAME>
 */
class DirectoryStyleRepository implements StyleRepository {

    private final File directory

    DirectoryStyleRepository(File directory) {
        this.directory = directory
    }

    @Override
    String getDefaultForLayer(String layerName) {
        getForLayer(layerName, layerName)
    }

    @Override
    String getForLayer(String layerName, String styleName) {
        ["sld","css"].findResult { String ext ->
            File file = new File(this.directory,"${styleName}.${ext}")
            if (file.exists()) {
                return file.text
            }
        }
    }

    @Override
    List<Map<String, String>> getForLayer(String layerName) {
        String defaultStyle = getDefaultForLayer(layerName)
        if (defaultStyle) {
            [
                [
                    layerName: layerName,
                    styleName: layerName,
                    style: defaultStyle
                ]
            ]
        } else {
            []
        }
    }

    @Override
    List<Map<String, String>> getAll() {
        this.directory.listFiles(new StyleFileNameFilter()).collect { File file ->
            [
                    layerName: FilenameUtils.getBaseName(file.name),
                    styleName: FilenameUtils.getBaseName(file.name),
                    style: file.text
            ]
        }
    }

    @Override
    void save(String layerName, String styleName, String style, Map options = [:]) {
        File file = new File(directory, "${styleName}.${options.get('type','sld')}")
        file.text = style
    }

    @Override
    void delete(String layerName, String styleName) {
        ["sld","css"].each { String ext ->
            File file = new File(directory, "${styleName}.${ext}")
            if (file.exists()) {
                file.delete()
            }
        }
    }

    private static class StyleFileNameFilter implements FilenameFilter {
        @Override
        boolean accept(File dir, String name) {
            name.endsWith(".sld") || name.endsWith(".css")
        }
    }

}
