Subversion Repositories mkgmap

Rev

Rev 3640 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2009 Christian Gawron
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *
 * Author: Christian Gawron
 * Create date: 03-Jul-2009
 */

package uk.me.parabola.mkgmap.reader.dem;

import java.io.File;
import java.io.FileNotFoundException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.FileSystemParam;
import uk.me.parabola.imgfmt.FormatException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.map.Map;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.build.MapBuilder;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.LoadableMapDataSource;
import uk.me.parabola.mkgmap.osmstyle.StyleImpl;
import uk.me.parabola.mkgmap.osmstyle.StyledConverter;
import uk.me.parabola.mkgmap.reader.MapperBasedMapDataSource;
import uk.me.parabola.mkgmap.reader.osm.OsmConverter;
import uk.me.parabola.mkgmap.reader.osm.Style;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.mkgmap.srt.SrtTextReader;
import uk.me.parabola.util.EnhancedProperties;


/**
 * Create contour lines using an algorithm similar to that described in <a
 * href="http://mapcontext.com/autocarto/proceedings/auto-carto-5/pdf/an-adaptive-grid-contouring-algorithm.pdf">An
 * Adaptive Grid Contouring Algorithm</a> by Downing and Zoraster.
 */

public abstract class DEM {
        private static final Logger log = Logger.getLogger(DEM.class);

        private final static double epsilon = 1e-9;
        final protected static double delta = 1.5;
        private final static int maxPoints = 200000;
        private final static double minDist = 15;
        private final static double maxDist = 21;

        protected static int M = 1200;
        protected static int N = M;
        protected static double res = 1.0 / N;
        private static int id = -1;

        protected int lat;
        protected int lon;

        protected abstract double ele(int x, int y);

        protected abstract void read(int minLon, int minLat, int maxLon, int maxLat);

        public static void createContours(LoadableMapDataSource mapData, EnhancedProperties config) {
                Area bounds = mapData.getBounds();

                double minLat = Utils.toDegrees(bounds.getMinLat());
                double minLon = Utils.toDegrees(bounds.getMinLong());
                double maxLat = Utils.toDegrees(bounds.getMaxLat());
                double maxLon = Utils.toDegrees(bounds.getMaxLong());

                System.out.printf("bounds: %f %f %f %f\n", minLat, minLon, maxLat, maxLon);
                DEM data;
                String demType = config.getProperty("dem-type", "SRTM");

                try {
                        String dataPath;
                        Class<?> demClass ;
                        switch (demType) {
                        case "ASTER":
                                dataPath = config.getProperty("dem-path", "ASTER");
                                demClass = Class.forName("uk.me.parabola.mkgmap.reader.dem.optional.GeoTiffDEM$ASTER");
                                break;
                        case "CGIAR":
                                dataPath = config.getProperty("dem-path", "CGIAR");
                                demClass = Class.forName("uk.me.parabola.mkgmap.reader.dem.optional.GeoTiffDEM$CGIAR");
                                break;
                        default:
                                dataPath = config.getProperty("dem-path", "SRTM");
                                demClass = Class.forName("uk.me.parabola.mkgmap.reader.dem.HGTDEM");
                                break;
                        }
                        Constructor<DEM> constructor = (Constructor<DEM>) demClass.getConstructor(String.class,
                                        Double.TYPE, Double.TYPE,
                                        Double.TYPE, Double.TYPE);
                        data = constructor.newInstance(dataPath, minLat, minLon, maxLat, maxLon);
                }
                catch (Exception ex) {
                        throw new ExitException("failed to create DEM", ex);
                }

                Isolines lines = data.new Isolines(data, minLat, minLon, maxLat, maxLon);
                int increment = config.getProperty("dem-increment", 10);

                double minHeight = lines.getMinHeight();
                double maxHeight = lines.getMaxHeight();
                int maxLevels = config.getProperty("dem-maxlevels", 100);
                while ((maxHeight - minHeight) / increment > maxLevels)
                        increment *= 2;

                Style style = StyleImpl.readStyle(config);

                LoadableMapDataSource dest = mapData;
                if (config.getProperty("dem-separate-img", false)) {
                        dest = new DEMMapDataSource(mapData, config);
                }

                OsmConverter converter = new StyledConverter(style, ((MapperBasedMapDataSource) dest).getMapper(), config);

                for (int level = 0; level < maxHeight; level += increment) {
                        if (level < minHeight) continue;

                        // create isolines
                        lines.addLevel(level);

                        for (Isolines.Isoline line : lines.isolines) {
                                Way way = new Way(id--, line.points);
                                way.addTag("contour", "elevation");
                                way.addTag("ele", String.format("%d", (int) line.level));
                                converter.convertWay(way);
                        }
                        lines.isolines.clear();
                }

                if (config.getProperty("dem-separate-img", false)) {
                        MapBuilder builder = new MapBuilder();
                        builder.config(config);

                        // Get output directory
                        String DEFAULT_DIR = ".";
                        String fileOutputDir = config.getProperty("output-dir", DEFAULT_DIR);
                       
                        // Test if directory exists
                        File outputDir = new File(fileOutputDir);
                        if (!outputDir.exists()) {
                                System.out.println("Output directory not found. Creating directory '" + fileOutputDir + "'");
                                if (!outputDir.mkdirs()) {
                                        System.err.println("Unable to create output directory! Using default directory instead");
                                        fileOutputDir = DEFAULT_DIR;
                                }
                        } else if (!outputDir.isDirectory()) {
                                System.err.println("The --output-dir parameter must specify a directory. The parameter is being ignored, writing to default directory instead.");
                                fileOutputDir = DEFAULT_DIR;
                        }              
                       
                        FileSystemParam params = new FileSystemParam();
                        params.setMapDescription("contour lines");
                        long mapName = Integer.valueOf(config.getProperty("mapname", "63240000"));
                        try {
                                String mapname = String.format("%08d", mapName + 10000000);
                                Map map = Map.createMap(mapname, fileOutputDir, params, mapname, SrtTextReader.sortForCodepage(1252));
                                builder.makeMap(map, dest);
                                map.close();
                        }
                        catch (Exception ex) {
                                throw new ExitException("could not open " + mapName, ex);
                        }
                }
        }

        private int lastXi = -1;
        private int lastYi = -1;

        private final static int[][] bcInv = {
                        {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                        {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
                        {-3, 0, 0, 3, 0, 0, 0, 0, -2, 0, 0, -1, 0, 0, 0, 0},
                        {2, 0, 0, -2, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0},
                        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0},
                        {0, 0, 0, 0, -3, 0, 0, 3, 0, 0, 0, 0, -2, 0, 0, -1},
                        {0, 0, 0, 0, 2, 0, 0, -2, 0, 0, 0, 0, 1, 0, 0, 1},
                        {-3, 3, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                        {0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2, -1, 0, 0},
                        {9, -9, 9, -9, 6, 3, -3, -6, 6, -6, -3, 3, 4, 2, 1, 2},
                        {-6, 6, -6, 6, -4, -2, 2, 4, -3, 3, 3, -3, -2, -1, -1, -2},
                        {2, -2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                        {0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 1, 1, 0, 0},
                        {-6, 6, -6, 6, -3, -3, 3, 3, -4, 4, 2, -2, -2, -2, -1, -1},
                        {4, -4, 4, -4, 2, 2, -2, -2, 2, -2, -2, 2, 1, 1, 1, 1}
        };

        private static int lastId = 1000000000;
        private static double lastX;
        private static double lastY;

        private static final int[][] off0 = {{0, 0},
                        {0, 0},
                        {0, 1},
                        {1, 1}};

        private static final int[][] off1 = {{1, 0},
                        {0, 1},
                        {1, 1},
                        {1, 0}};

        private static final int[] brd = {1, 2, 4, 8};

        private static final int[] rev = {2, 3, 0, 1};

        private static final int[][] mov = {{0, -1},
                        {-1, 0},
                        {0, 1},
                        {1, 0}};


        private final double[][] bc = new double[4][4];
        private final double[] bc_y = new double[4];
        private final double[] bc_y1 = new double[4];
        private final double[] bc_y2 = new double[4];
        private final double[] bc_y12 = new double[4];
        private final double[] bc_Coeff = new double[16];
        private final double[] bc_x = new double[16];

        private void recalculateCoefficients(int xi, int yi) {

                double v00 = ele(xi, yi);
                double v0p = ele(xi, yi + 1);
                double vpp = ele(xi + 1, yi + 1);
                double vp0 = ele(xi + 1, yi);

                double vm0 = ele(xi - 1, yi);
                double v0m = ele(xi, yi - 1);
                double vmp = ele(xi - 1, yi + 1);
                double vpm = ele(xi + 1, yi - 1);
                double vmm = ele(xi - 1, yi - 1);
                double vmP = ele(xi + 2, yi - 1);
                double vPm = ele(xi - 1, yi + 2);

                double vP0 = ele(xi + 2, yi);
                double v0P = ele(xi, yi + 2);
                double vPp = ele(xi + 2, yi + 1);
                double vpP = ele(xi + 1, yi + 2);
                double vPP = ele(xi + 2, yi + 2);

                bc_y[0] = v00;
                bc_y[1] = vp0;
                bc_y[2] = vpp;
                bc_y[3] = v0p;

                bc_y1[0] = (vp0 - vm0) / 2;
                bc_y1[1] = (vP0 - v00) / 2;
                bc_y1[2] = (vPp - v0p) / 2;
                bc_y1[3] = (vpp - vmp) / 2;

                bc_y2[0] = (v0p - v0m) / 2;
                bc_y2[1] = (vpp - vpm) / 2;
                bc_y2[2] = (vpP - vp0) / 2;
                bc_y2[3] = (v0P - v00) / 2;

                bc_y12[0] = (vpp - vpm - vmp + vmm) / 4;
                bc_y12[0] = (vPp - vPm - v0p + v0m) / 4;
                bc_y12[2] = (vPP - vP0 - v0P + v00) / 4;
                bc_y12[0] = (vpP - vp0 - vmP + vm0) / 4;

                int i;

                for (i = 0; i < 4; i++) {
                        bc_x[i] = bc_y[i];
                        bc_x[i + 4] = bc_y1[i];
                        bc_x[i + 8] = bc_y2[i];
                        bc_x[i + 12] = bc_y12[i];
                }

                for (i = 0; i < 16; i++) {
                        double s = 0;
                        for (int k = 0; k < 16; k++) s += bcInv[i][k] * bc_x[k];
                        bc_Coeff[i] = s;
                }

                int l = 0;
                for (i = 0; i < 4; i++)
                        for (int j = 0; j < 4; j++)
                                bc[i][j] = bc_Coeff[l++];
        }

        protected double gradient(double lat, double lon, double[] grad) {
                grad[0] = 0;
                grad[1] = 0;

                double x = (lon - this.lon) / res;
                double y = (lat - this.lat) / res;

                int xi = (int) x;
                int yi = (int) y;

                if (lastXi != xi || lastYi != yi) {
                        log.debug("new Cell for interpolation: %d %d", xi, yi);
                        recalculateCoefficients(xi, yi);
                        lastXi = xi;
                        lastYi = yi;
                }

                double t = x - xi;
                double u = y - yi;

                if (xi < 0 || xi > N + 1 || yi < 0 || yi > N + 1)
                        throw new IndexOutOfBoundsException(String.format("(%f, %f)->(%d, %d)", lat, lon, xi, yi));

                double val = 0;
                for (int i = 3; i >= 0; i--) {
                        val = t * val + ((bc[i][3] * u + bc[i][2]) * u + bc[i][1]) * u + bc[i][0];
                        grad[0] = u * grad[0] + (3 * bc[3][i] * t + 2 * bc[2][i]) * t + bc[1][i];
                        grad[1] = t * grad[1] + (3 * bc[i][3] * t + 2 * bc[i][2]) * t + bc[i][1];
                }

                return val;
        }

        protected double elevation(double lat, double lon) {
                double x = (lon - this.lon) / res;
                double y = (lat - this.lat) / res;

                int xi = (int) x;
                int yi = (int) y;

                if (lastXi != xi || lastYi != yi) {
                        log.debug("new Cell for interpolation: %d %d", xi, yi);
                        recalculateCoefficients(xi, yi);
                        lastXi = xi;
                        lastYi = yi;
                }

                double t = x - xi;
                double u = y - yi;

                if (xi < 0 || xi > N + 1 || yi < 0 || yi > N + 1)
                        throw new IndexOutOfBoundsException(String.format("(%f, %f)->(%d, %d)", lat, lon, xi, yi));

                double val = 0;
                for (int i = 3; i >= 0; i--) {
                        val = t * val + ((bc[i][3] * u + bc[i][2]) * u + bc[i][1]) * u + bc[i][0];
                }

                return val;
        }

        protected double elevation(int x, int y) {
                if (x < 0 || x > N || y < 0 || y > N)
                        throw new IndexOutOfBoundsException(String.format("elevation: %d %d", x, y));
                return ele(x, y);
        }


        class Isolines {
                final DEM data;
                final int minX;
                final int maxX;
                final int minY;
                final int maxY;

                double min;
                double max;

                final ArrayList<Isoline> isolines = new ArrayList<>();

                class Isoline {
                        final int id;
                        final ArrayList<Coord> points;
                        final double level;

                        private Isoline(double level) {
                                this.level = level;
                                id = lastId++;
                                points = new ArrayList<>();
                        }

                        private class Edge implements Brent.Function {
                                final double x0;
                                final double y0;
                                final double x1;
                                final double y1;

                                Edge(double x0, double y0, double x1, double y1) {
                                        this.x0 = x0;
                                        this.y0 = y0;
                                        this.x1 = x1;
                                        this.y1 = y1;
                                }

                                public double eval(double d) {
                                        return data.elevation(x0 + d * (x1 - x0), y0 + d * (y1 - y0)) - level;
                                }
                        }

                        private class FN implements Brent.Function {
                                double x0, y0;
                                double dx, dy;

                                public void setParameter(double x0, double y0, double dx, double dy) {
                                        this.x0 = x0;
                                        this.y0 = y0;
                                        this.dx = dx;
                                        this.dy = dy;
                                }

                                public double eval(double t) {
                                        return data.elevation(y0 + t * dy, x0 + t * dx) - level;
                                }
                        }

                        private final FN fn = new FN();

                        final double[] grad = new double[2];
                        final double[] px = new double[4];
                        final double[] py = new double[4];
                        final int[] edges = new int[4];

                        boolean addCell(Position p, int direction) {
                                log.debug("addCell: %f %d %d %d %d", level, p.ix, p.iy, p.edge, direction);

                                int c = 0;
                                for (int k = 0; k < 4; k++) {
                                        if (k == p.edge)
                                                continue;

                                        int x0 = p.ix + off0[k][0];
                                        int y0 = p.iy + off0[k][1];
                                        int x1 = p.ix + off1[k][0];
                                        int y1 = p.iy + off1[k][1];

                                        double l0 = elevation(x0, y0) - level;
                                        double l1 = elevation(x1, y1) - level;

                                        if (Math.abs(l1) < epsilon || l0 * l1 < 0) {
                                                edges[c] = k;

                                                Brent.Function f = new Edge(data.lat + y0 * DEM.res, data.lon + x0 * DEM.res, data.lat + y1 * DEM.res, data.lon + x1 * DEM.res);
                                                double f0 = elevation(x0, y0) - level;
                                                double delta;

                                                if (Math.abs(1) < epsilon) {
                                                        delta = 1;
                                                } else if (Math.abs(f0) < epsilon)
                                                        throw new ExitException("implementation error!");
                                                else
                                                        delta = Brent.zero(f, epsilon, 1 - epsilon);

                                                px[c] = data.lon + (x0 + delta * (x1 - x0)) * DEM.res;
                                                py[c] = data.lat + (y0 + delta * (y1 - y0)) * DEM.res;
                                                c++;
                                        }
                                }


                                if (c == 1) {
                                        p.edge = edges[0];

                                        double px0 = p.x;
                                        double py0 = p.y;
                                        p.x = px[0];
                                        p.y = py[0];
                                        double px1 = p.x;
                                        double py1 = p.y;

                                        double xMin = data.lon + p.ix * DEM.res;
                                        double xMax = xMin + DEM.res;
                                        double yMin = data.lat + p.iy * DEM.res;
                                        double yMax = yMin + DEM.res;

                                        refineAdaptively(xMin, yMin, xMax, yMax, px0, py0, px1, py1, direction, maxDist);

                                        addPoint(p.x, p.y, direction);
                                        p.moveCell();
                                        return true;
                                } else {
                                        log.debug("addCellByStepping: %d", c);
                                        return addCellByStepping(p, direction, c, edges, px, py);
                                }
                        }

                        private void refineAdaptively(double xMin, double yMin, double xMax, double yMax,
                                        double x0, double y0, double x1, double y1,
                                        int direction, double maxDist)
                        {
                                double dist = quickDistance(x0, y0, x1, y1);
                                if (dist > maxDist) {
                                        double dx = x1 - x0;
                                        double dy = y1 - y0;

                                        double xm = x0 + 0.5 * dx;
                                        double ym = y0 + 0.5 * dy;
                                        double n = Math.sqrt(dx * dx + dy * dy);
                                        fn.setParameter(xm, ym, -dy / n, dx / n);
                                        Brent.Function f = fn;
                                        double t0 = -0.05 * res;
                                        double t1 = 0.05 * res;
                                        double f0 = f.eval(t0);
                                        double f1 = f.eval(t1);

                                        int count = 0;
                                        while (f0 * f1 > 0 && count++ < 20) {
                                                if ((count & 1) > 0)
                                                        t0 -= 0.05 * res;
                                                else
                                                        t1 += 0.05 * res;
                                                f0 = f.eval(t0);
                                                f1 = f.eval(t1);
                                                log.debug("refine: %f %f %f %f", t0, t1, f0, f1);
                                        }

                                        if (f0 * f1 < 0) {
                                                double t = Brent.zero(f, t0, t1);
                                                xm -= t * dy;
                                                ym += t * dx;
                                        } else {
                                                log.debug("refine failed: %f %f %f %f", t0, t1, f0, f1);
                                                return;
                                        }

                                        if (xm > xMin && xm < xMax && ym > yMin && ym < yMax)
                                                refineAdaptively(xMin, yMin, xMax, yMax, x0, y0, xm, ym, direction, maxDist * 1.1);
                                        addPoint(xm, ym, direction);
                                        if (xm > xMin && xm < xMax && ym > yMin && ym < yMax)
                                                refineAdaptively(xMin, yMin, xMax, yMax, xm, ym, x1, y1, direction, maxDist * 1.1);
                                }
                        }

                        boolean addCellByStepping(Position p, int direction, int numEdges, int[] edges, double[] px, double[] py) {
                                log.debug("addCellByStepping: %f %d %d %d %d", level, p.ix, p.iy, p.edge, direction);

                                int iMin = -1;
                                double md = 5000;

                                for (int i = 0; i < numEdges; i++) {
                                        gradient(p.y, p.x, grad);
                                        double dist = quickDistance(p.x, p.y, px[i], py[i]);
                                        log.debug("distance %d: %f", i, dist);

                                        if (dist < md && (visited[p.iy * (N + 1) + p.ix] & brd[edges[i]]) == 0) {
                                                md = dist;
                                                iMin = i;
                                        }
                                }

                                p.edge = edges[iMin];

                                double px0 = p.x;
                                double py0 = p.y;
                                p.x = px[iMin];
                                p.y = py[iMin];
                                double px1 = p.x;
                                double py1 = p.y;

                                double xMin = data.lon + p.ix * DEM.res;
                                double xMax = xMin + DEM.res;
                                double yMin = data.lat + p.iy * DEM.res;
                                double yMax = yMin + DEM.res;

                                refineAdaptively(xMin, yMin, xMax, yMax, px0, py0, px1, py1, direction, maxDist);

                                addPoint(p.x, p.y, direction);
                                p.moveCell();
                                return true;
                        }

                        private void addPoint(double x, double y, int direction) {
                                double dist = quickDistance(x, y, lastX, lastY);
                                log.debug("addPoint: %f %f %f", x, y, dist);

                                if (dist > minDist) {
                                        if (direction > 0)
                                                points.add(0, new Coord(y, x));
                                        else
                                                points.add(points.size(), new Coord(y, x));
                                        lastX = x;
                                        lastY = y;
                                }
                        }

                        private void close() {
                                points.add(points.size(), points.get(0));
                        }

                }

                public Isolines(DEM data, double minLat, double minLon, double maxLat, double maxLon) {
                        System.out.printf("init: %f %f %f %f\n", minLat, minLon, maxLat, maxLon);

                        this.data = data;
                        this.minX = (int) ((minLon - data.lon) / res);
                        this.minY = (int) ((minLat - data.lat) / res);
                        this.maxX = (int) ((maxLon - data.lon) / res);
                        this.maxY = (int) ((maxLat - data.lat) / res);

                        init();
                }

                private void init() {
                        System.out.printf("init: %d %d %d %d\n", minX, minY, maxX, maxY);
                        data.read(minX - 2, minY - 2, maxX + 2, maxY + 2);
                        // we need some overlap for bicubic interpolation
                        max = -1000;
                        min = 10000;
                        for (int i = minX; i < maxX; i++)
                                for (int j = minY; j < maxY; j++) {
                                        if (data.elevation(i, j) < min) min = data.elevation(i, j);
                                        if (data.elevation(i, j) > max) max = data.elevation(i, j);
                                }

                        log.debug("min: %f, max: %f\n", min, max);
                }

                double getMinHeight() {
                        return min;
                }

                double getMaxHeight() {
                        return max;
                }

                private class Edge implements Brent.Function {
                        final double x0, y0, x1, y1, level;

                        Edge(double x0, double y0, double x1, double y1, double level) {
                                this.x0 = x0;
                                this.y0 = y0;
                                this.x1 = x1;
                                this.y1 = y1;
                                this.level = level;
                        }

                        public double eval(double d) {
                                return data.elevation(x0 + d * (x1 - x0), y0 + d * (y1 - y0)) - level;
                        }
                }

                private class Position {
                        int ix, iy;
                        double x, y;
                        int edge;

                        Position(int ix, int iy, double x, double y, int edge) {
                                this.ix = ix;
                                this.iy = iy;
                                this.x = x;
                                this.y = y;
                                this.edge = edge;
                        }

                        Position(Position p) {
                                this.ix = p.ix;
                                this.iy = p.iy;
                                this.x = p.x;
                                this.y = p.y;
                                this.edge = p.edge;
                        }

                        void markEdge() {
                                log.debug("marking edge: %d %d %d %d", ix, iy, edge, brd[edge]);
                                visited[iy * (N + 1) + ix] |= brd[edge];
                        }

                        void moveCell() {
                                markEdge();
                                ix += mov[edge][0];
                                iy += mov[edge][1];
                                edge = rev[edge];
                                markEdge();
                        }
                }

                final byte[] visited = new byte[(N + 1) * (N + 1)];

                public void addLevel(double level) {
                        if (level < min || level > max)
                                return;

                        System.out.printf("addLevel: %f\n", level);
                        Arrays.fill(visited, (byte) 0);

                        for (int y = minY; y < maxY; y++) {
                                for (int x = minX; x < maxX; x++) {
                                        byte v = 0;
                                        int direction;
                                        // Mark the borders of the cell, represented by the four points (i, j), (i+1, j), (i, j+1), (i+1, j+1),
                                        // which are intersected by the contour. The values are:
                                        // 1: top
                                        // 2: left
                                        // 4: bottom
                                        // 8: right
                                        if (data.elevation(x, y) >= level) {
                                                if (data.elevation(x + 1, y) < level) {
                                                        v |= 1;
                                                }
                                                if (data.elevation(x, y + 1) < level) {
                                                        v |= 2;
                                                }
                                                direction = 1;
                                        } else {
                                                if (data.elevation(x + 1, y) > level) {
                                                        v |= 1;
                                                }
                                                if (data.elevation(x, y + 1) > level) {
                                                        v |= 2;
                                                }
                                                direction = -1;
                                        }

                                        int k = -1;

                                        if ((v & 1) > 0 && (visited[y * (N + 1) + x] & 1) == 0) {
                                                k = 0;
                                        } else if ((v & 2) > 0 && (visited[y * (N + 1) + x] & 2) == 0) {
                                                k = 1;
                                        }

                                        if (k >= 0) {
                                                int x0 = x + off0[k][0];
                                                int y0 = y + off0[k][1];
                                                int x1 = x + off1[k][0];
                                                int y1 = y + off1[k][1];

                                                try {
                                                        Brent.Function f = new Edge(data.lat + y0 * DEM.res, data.lon + x0 * DEM.res,
                                                                        data.lat + y1 * DEM.res, data.lon + x1 * DEM.res,
                                                                        level);
                                                        double f0 = elevation(x0, y0) - level;
                                                        double f1 = elevation(x1, y1) - level;
                                                        double delta;
                                                        if (Math.abs(f0) < epsilon) {
                                                                delta = 0;
                                                        } else if (Math.abs(f1) < epsilon)
                                                                continue;
                                                        else
                                                                delta = Brent.zero(f, 0, 1 - epsilon);

                                                        Position p = new Position(x, y, data.lon + (x0 + delta * (x1 - x0)) * DEM.res, data.lat + (y0 + delta * (y1 - y0)) * DEM.res, k);
                                                        p.markEdge();
                                                        isolines.add(traceByStepping(level, p, direction));
                                                }
                                                catch (RuntimeException ex) {
                                                        log.debug("error: %s", ex.toString());
                                                        ex.printStackTrace();
                                                        return;
                                                }
                                        }
                                }
                        }
                }

                private Isoline traceByStepping(double level, Position p, int direction) {
                        log.debug("traceByStepping: starting contour %f %d %d %f %f %d", level, p.ix, p.iy, p.x, p.y, p.edge);
                        int n = 0;
                        Position startP = new Position(p);
                        boolean foundEnd = false;

                        Isoline line = new Isoline(level);

                        while (true) {
                                log.debug("traceByStepping: %f %d %d %f %f %d", level, p.ix, p.iy, p.x, p.y, p.edge);
                                visited[p.iy * (N + 1) + p.ix] |= brd[p.edge];

                                if (n > 0 && p.ix == startP.ix && p.iy == startP.iy && quickDistance(p.x, p.y, startP.x, startP.y) < 5) {
                                        log.debug("closed curve!");
                                        line.close();
                                        break;
                                } else if (p.ix < minX || p.iy < minY || p.ix >= maxX || p.iy >= maxY) {
                                        if (foundEnd) // did we already reach one end?
                                        {
                                                log.debug("second border reached!");
                                                break;
                                        } else {
                                                log.debug("border reached!");
                                                foundEnd = true;
                                                n = 0;
                                                direction *= -1;
                                                p = new Position(startP);
                                                p.moveCell();
                                                continue;
                                        }
                                }
                                n++;
                                if (!line.addCell(p, direction) || line.points.size() > maxPoints) {
                                        log.debug("ending contour");
                                        isolines.add(line);
                                        return line;
                                }
                        }

                        return line;
                }

        }

        private static double quickDistance(double long1, double lat1, double long2, double lat2) {
                double latDiff;
                if (lat1 < lat2)
                        latDiff = lat2 - lat1;
                else
                        latDiff = lat1 - lat2;
                if (latDiff > 90)
                        latDiff -= 180;

                double longDiff;
                if (long1 < long2)
                        longDiff = long2 - long1;
                else
                        longDiff = long1 - long2;
                if (longDiff > 180)
                        longDiff -= 360;

                // scale longDiff by cosine of average latitude
                longDiff *= Math.cos(Math.PI / 180 * Math.abs((lat1 + lat2) / 2));

                double distDegSq = (latDiff * latDiff) + (longDiff * longDiff);

                return 40075000 * Math.sqrt(distDegSq) / 360;
        }


        private static class DEMMapDataSource extends MapperBasedMapDataSource implements LoadableMapDataSource {
                final LoadableMapDataSource parent;
                final List<String> copyright = new ArrayList<>();

                DEMMapDataSource(LoadableMapDataSource parent, EnhancedProperties props) {
                        this.parent = parent;
                        config(props);
                }

                public boolean isFileSupported(String name) {
                        return false;
                }

                public void load(String name)
                                throws FileNotFoundException, FormatException
                {
                        throw new FormatException("load not supported");
                }

                public LevelInfo[] mapLevels() {
                        return parent.mapLevels();
                }

                public LevelInfo[] overviewMapLevels() {
                        return parent.overviewMapLevels();
                }

                public String[] copyrightMessages() {
                        return copyright.toArray(new String[1]);
                }

        }
}