All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dstadler.ctw.tiles.CreateTileOverlaysHelper Maven / Gradle / Ivy

package org.dstadler.ctw.tiles;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;

import javax.imageio.ImageIO;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.dstadler.commons.collections.ConcurrentMappedCounter;
import org.dstadler.commons.logging.jdk.LoggerFactory;
import org.dstadler.ctw.geotools.GeoTools;
import org.dstadler.ctw.utils.Constants;
import org.dstadler.ctw.utils.LatLonRectangle;
import org.dstadler.ctw.utils.OSMTile;
import org.geotools.feature.FeatureCollection;

import com.google.common.base.Preconditions;
import com.pngencoder.PngEncoder;

/**
 * Functionality which is shared by the applications which
 * create overlay pngs for covered tiles and squares.
 */
public class CreateTileOverlaysHelper {
	private static final Logger log = LoggerFactory.make();

	// Create a GradientPaint for this direction and size
	private static final int RGB = new Color(255, 0, 0, 80).getRGB();

	// most arrays will be full, so let's re-use them to save main memory
	protected static final boolean[][] FULL = new boolean[256][256];
	static {
		for (int x = 0; x < 256; x++) {
			for (int y = 0; y < 256; y++) {
				FULL[x][y] = true;
			}
		}
	}

	// for printing stats when writing tiles
	private static final AtomicLong lastLog = new AtomicLong();

	protected static final ConcurrentMappedCounter EXPECTED = new ConcurrentMappedCounter<>();
	protected static final ConcurrentMappedCounter ACTUAL = new ConcurrentMappedCounter<>();

	protected static Set read(String file, String logName) throws IOException {
		Set lines = new TreeSet<>();
		try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
			while (true) {
				String line = reader.readLine();
				if (line == null) {
					break;
				}

				// 33U 608000.0 5337000.0,33U 609000.0 5338000.0
				lines.add(line);
			}
		}

		log.info("Found " + lines.size() + " covered " + logName);

		return lines;
	}

	protected static void cleanTiles(File tileDir) {
		if (tileDir.exists()) {
			log.info("Removing previous tiles at " + tileDir);
			Arrays.stream(Objects.requireNonNull(tileDir.listFiles())).forEach(s -> {
				try {
					FileUtils.deleteDirectory(s);
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			});
		}
	}

	public static boolean isFull(boolean[][] pixel) {
		boolean full = true;
		for (int x = 0; x < 256; x++) {
			for (int y = 0; y < 256; y++) {
				if (!pixel[x][y]) {
					full = false;
					break;
				}
			}
			if (!full) {
				break;
			}
		}
		return full;
	}

	public static void writePixel(Map tiles, OSMTile tile, LatLonRectangle recTileIn) {
		// ensure the tile and it's pixel-array are in the map
		boolean[][] pixel = tiles.computeIfAbsent(tile, osmTile -> new boolean[256][256]);

		if (pixel == FULL) {
			// already full, nothing to do anymore
			return;
		}

		LatLonRectangle recTile = tile.getRectangle();

		// compute how much of the tileIn is located in this tile
		// so that we can fill the boolean-buffer accordingly
		LatLonRectangle recResult = recTileIn.intersect(recTile);
		Preconditions.checkNotNull(recResult,
				"Expected to have an intersection of rectangles %s and %s",
				recTileIn, recTile);

		//log.info("For '" + tile + "', zoom " + zoom + " and xy " + x + "/" + y + ": Had rect " + recResult + " for " + recTile + " and " + recTile);
		fillPixel(recResult, pixel, tile, true);

		// replace a "full" array with a global instance to save main memory
		if (isFull(pixel)) {
			tiles.put(tile, FULL);
		}
	}

	private static void fillPixel(LatLonRectangle recResult, boolean[][] pixel, OSMTile tile, boolean expand) {
		Pair pixelStart = getAndCheckPixel(recResult.lat1, recResult.lon1, tile);
		Pair pixelEnd = getAndCheckPixel(recResult.lat2, recResult.lon2, tile);

		// add some pixel to avoid strange artefacts caused by changing sizes depending on latitude
		int endX = Math.min(255, pixelEnd.getKey() + (expand ? expandPixel(tile.getZoom()) : 0));
		int endY = pixelEnd.getValue();
		int startX = pixelStart.getKey();
		int startY = pixelStart.getValue();
		Preconditions.checkState(startX <= endX,
				"Having: pixelStart: %s and pixelEnd %s for recResult: %s",
				pixelStart, pixelEnd, recResult);
		Preconditions.checkState(startY <= endY,
				"Having: pixelStart: %s and pixelEnd %s for recResult: %s",
				pixelStart, pixelEnd, recResult);

		for (int xPixel = startX; xPixel <= endX; xPixel++) {
			for (int yPixel = startY; yPixel <= endY; yPixel++) {
				pixel[xPixel][yPixel] = true;
			}
		}
	}

	private static int expandPixel(int zoom) {
		switch (zoom) {
			case 13:
				return 1;
			case 14:
				return 2;
			case 15:
				return 4;
			case 16:
				return 9;
			case 17:
				return 18;
			case 18:
				return 36;
			default:
				return 0;
		}
	}

	public static void writeBorderPixel(Map tiles, OSMTile tile, LatLonRectangle recArea) {
		// ensure the tile and it's pixel-array are in the map
		boolean[][] pixel = tiles.computeIfAbsent(tile, osmTile -> new boolean[256][256]);

		if (pixel == FULL) {
			// already full, nothing to do anymore
			return;
		}

		LatLonRectangle recTile = tile.getRectangle();

		// compute coordinates of borders of the tile in the drawing-area
		List borders = recTile.borderInside(recArea);

		for (LatLonRectangle border : borders) {
			fillPixel(border, pixel, tile, false);
		}
	}

	protected static void writeTilesToFiles(File combinedDir, Map tiles, File tileDir, int zoom) throws IOException {
		int tileCount = tiles.size();
		int tileNr = 1;

		Iterator> it = tiles.entrySet().iterator();

		while (it.hasNext()) {
			Map.Entry entry = it.next();

			// reduce memory usage by removing items quickly from map again
			// so the map cannot be used any more afterwards
			it.remove();

			File file = entry.getKey().toFile(tileDir);
			try {
				boolean written = writePNG(file, entry.getValue());

				// whenever writing a tile, remove the combined overlay to re-create it in a follow-up step
				if (written) {
					File combinedTile = new File(combinedDir, entry.getKey().toCoords() + ".png");
					if (combinedTile.exists()) {
						if (!combinedTile.delete()) {
							throw new IOException("Could not delete file " + combinedTile);
						}
					}
				}

				if (lastLog.get() + TimeUnit.SECONDS.toMillis(5) < System.currentTimeMillis()) {
					log.info(String.format(Locale.US, "bool[] -> png: zoom %d: %,d of %,d: %s%s",
							zoom, tileNr, tileCount, file, concatProgress()));

					lastLog.set(System.currentTimeMillis());
				}
			} catch (IOException e) {
				throw new IOException("While handling file: " + file, e);
			}

			tileNr++;
			ACTUAL.inc(zoom);
		}
	}

	public static boolean writePNG(File file, boolean[][] pixel) throws IOException {
		BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);

		for (int x = 0; x < 256; x++) {
			for (int y = 0; y < 256; y++) {
				if (pixel[x][y]) {
					image.setRGB(x, y, RGB);
					//g.fillRect(x, y, x, y);
				}
			}
		}

		// Save the image in PNG format using the javax.imageio API
		if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
			throw new IOException("Could not create directory at " + file.getParentFile());
		}

		// skip if existing Image is equal to not change the "last modified" date
		if (file.exists()) {
			if (imagesEqual(file, image)) {
				return false;
			}
		}

		new PngEncoder()
				.withBufferedImage(image)
				.withCompressionLevel(1)
				.toFile(file);

		return true;
	}

	protected static boolean imagesEqual(File file, BufferedImage image) throws IOException {
		BufferedImage existing = ImageIO.read(file);

		int[] a1 = existing.getData().getPixels(0, 0, 256, 256, (int[])null);
		int[] a2 = image.getData().getPixels(0, 0, 256, 256, (int[])null);

		return Arrays.compare(a1, a2) == 0;
	}

	public static Pair getAndCheckPixel(double lat, double lon, OSMTile tile) {
		Pair pixel = tile.getPixelInTile(lat, lon);
		Preconditions.checkState(pixel.getKey() >= 0 && pixel.getKey() < 256,
				"Had: %s", pixel);
		Preconditions.checkState(pixel.getValue() >= 0 && pixel.getValue() < 256,
				"Had: %s", pixel);
		return pixel;
	}

	public static void writeTilesToFiles(File combinedDir, Set tilesOut, File tileDir,
			FeatureCollection features, boolean borderOnly) throws IOException {
		int tilesNr = 1;
		for (OSMTile tile : tilesOut) {
			writeTileToFile(combinedDir, tilesOut, tileDir, features, tile, tilesNr, borderOnly);

			tilesNr++;
			CreateTileOverlaysHelper.ACTUAL.inc(tile.getZoom());
		}
	}

	private static void writeTileToFile(File combinedDir,
			Set tilesOut,
			File tileDir,
			FeatureCollection features,
			OSMTile tile,
			int tilesNr,
			boolean borderOnly) throws IOException {
		File file = tile.toFile(tileDir);

		// Save the image in PNG format using the javax.imageio API
		if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
			throw new IOException("Could not create directory at " + file.getParentFile());
		}

		if (borderOnly) {
			GeoTools.writeBorder(features, tile.getRectangle(), file);
		} else {
			GeoTools.writeImage(features, tile.getRectangle(), file);
		}

		// whenever writing a tile, remove the combined overlay to re-create it in a follow-up step
		/*if (written)*/
		{
			File combinedTile = new File(combinedDir, tile.toCoords() + ".png");
			if (combinedTile.exists()) {
				if (!combinedTile.delete()) {
					throw new IOException("Could not delete file " + combinedTile);
				}
			}
		}

		if (lastLog.get() + TimeUnit.SECONDS.toMillis(5) < System.currentTimeMillis()) {
			log.info(String.format(Locale.US, "features -> png: zoom %d: %,d of %,d: %s%s",
					tile.getZoom(), tilesNr, tilesOut.size(), tile.toCoords(), CreateTileOverlaysHelper.concatProgress()));

			lastLog.set(System.currentTimeMillis());
		}
	}

	protected static String concatProgress() {
		StringBuilder progress = new StringBuilder();
		for (int zoom = Constants.MIN_ZOOM; zoom <= Constants.MAX_ZOOM; zoom++) {
			long actual = ACTUAL.get(zoom);
			if (actual == -1) {
				progress.append(", ").append(zoom).append(":_");
				continue;
			}

			long expected = EXPECTED.get(zoom);

			// include if not started yet
			if (expected == 0) {
				progress.append(", ").append(zoom).append(":0%");
			} else if (actual != expected) {
				// otherwise include if not completed yet
				progress.append(
						String.format(", %d:%.0f%%",
								zoom, ((double) actual) / expected * 100));
			}
		}

		return progress.toString();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy