diff --git a/src/main/java/org/dynmap/hdmap/DummyHDRenderer.java b/src/main/java/org/dynmap/hdmap/DummyHDRenderer.java new file mode 100644 index 00000000..e741e630 --- /dev/null +++ b/src/main/java/org/dynmap/hdmap/DummyHDRenderer.java @@ -0,0 +1,45 @@ +package org.dynmap.hdmap; + +import static org.dynmap.JSONUtils.a; +import static org.dynmap.JSONUtils.s; + +import java.io.File; + +import org.dynmap.ColorScheme; +import org.dynmap.ConfigurationNode; +import org.dynmap.Log; +import org.dynmap.kzedmap.KzedMapTile; +import org.dynmap.kzedmap.DefaultTileRenderer.BiomeColorOption; +import org.dynmap.utils.MapChunkCache; +import org.json.simple.JSONObject; + +public class DummyHDRenderer implements HDMapTileRenderer { + private ConfigurationNode configuration; + private String name; + + public DummyHDRenderer(ConfigurationNode configuration) { + this.configuration = configuration; + name = (String) configuration.get("prefix"); + } + public boolean isBiomeDataNeeded() { return false; } + public boolean isRawBiomeDataNeeded() { return false; }; + public boolean isNightAndDayEnabled() { return false; } + public String getName() { return name; } + + public boolean render(MapChunkCache cache, HDMapTile tile, File outputFile) { + Log.info("DummyHDRenderer(" + tile + ", " + outputFile.getPath()); + return false; + } + + @Override + public void buildClientConfiguration(JSONObject worldObject) { + ConfigurationNode c = configuration; + JSONObject o = new JSONObject(); + s(o, "type", "HDMapType"); + s(o, "name", c.getString("name")); + s(o, "title", c.getString("title")); + s(o, "icon", c.getString("icon")); + s(o, "prefix", c.getString("prefix")); + a(worldObject, "maps", o); + } +} diff --git a/src/main/java/org/dynmap/hdmap/HDMap.java b/src/main/java/org/dynmap/hdmap/HDMap.java new file mode 100644 index 00000000..183a3482 --- /dev/null +++ b/src/main/java/org/dynmap/hdmap/HDMap.java @@ -0,0 +1,566 @@ +package org.dynmap.hdmap; + +import org.dynmap.DynmapWorld; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.World.Environment; +import org.dynmap.Client; +import org.dynmap.Color; +import org.dynmap.ColorScheme; +import org.dynmap.ConfigurationNode; +import org.dynmap.DynmapChunk; +import org.dynmap.Log; +import org.dynmap.MapManager; +import org.dynmap.MapTile; +import org.dynmap.MapType; +import org.dynmap.TileHashManager; +import org.dynmap.debug.Debug; +import org.dynmap.flat.FlatMap.FlatMapTile; +import org.dynmap.kzedmap.KzedMap.KzedBufferedImage; +import org.dynmap.kzedmap.KzedMap; +import org.dynmap.kzedmap.MapTileRenderer; +import org.dynmap.utils.FileLockManager; +import org.dynmap.utils.MapChunkCache; +import org.dynmap.utils.MapIterator; +import org.dynmap.utils.Matrix3D; +import org.dynmap.utils.Vector3D; +import org.json.simple.JSONObject; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBuffer; +import java.awt.image.WritableRaster; +import java.awt.image.ColorModel; +import java.awt.image.Raster; + +public class HDMap extends MapType { + /* View angles */ + public double azimuth; /* Angle in degrees from looking north (0), east (90), south (180), or west (270) */ + public double inclination; /* Angle in degrees from horizontal (0) to vertical (90) */ + public double scale; /* Scale - tile pixel widths per block */ + public ColorScheme colorScheme; + /* Coordinate space for tiles consists of a plane (X, Y), corresponding to the projection of each tile on to the + * plane of the bottom of the world (X positive to the right, Y positive to the top), with Z+ corresponding to the + * height above this plane on a vector towards the viewer). Logically, this makes the parallelogram representing the + * space contributing to the tile have consistent tile-space X,Y coordinate pairs for both the top and bottom faces + * Note that this is a classic right-hand coordinate system, while minecraft's world coordinates are left handed + * (X+ is south, Y+ is up, Z+ is east). + */ + /* Transformation matrix for taking coordinate in world-space (x, y, z) and finding coordinate in tile space (x, y, z) */ + private Matrix3D world_to_map; + private Matrix3D map_to_world; + + /* dimensions of a map tile */ + public static final int tileWidth = 128; + public static final int tileHeight = 128; + + /* Maximum and minimum inclinations */ + public static final double MAX_INCLINATION = 90.0; + public static final double MIN_INCLINATION = 20.0; + + /* Maximum and minimum scale */ + public static final double MAX_SCALE = 64; + public static final double MIN_SCALE = 1; + + private HDMapTileRenderer renderers[]; + + public HDMap(ConfigurationNode configuration) { + colorScheme = ColorScheme.getScheme(configuration.getString("colorscheme", "default")); + + azimuth = configuration.getDouble("azimuth", 135.0); /* Get azimuth (default to classic kzed POV */ + inclination = configuration.getDouble("inclination", 60.0); + if(inclination > MAX_INCLINATION) inclination = MAX_INCLINATION; + if(inclination < MIN_INCLINATION) inclination = MIN_INCLINATION; + scale = configuration.getDouble("scale", MIN_SCALE); + if(scale < MIN_SCALE) scale = MIN_SCALE; + if(scale > MAX_SCALE) scale = MAX_SCALE; + Log.info("azimuth=" + azimuth + ", inclination=" + inclination + ", scale=" + scale); + + /* Generate transform matrix for world-to-tile coordinate mapping */ + /* First, need to fix basic coordinate mismatches before rotation - we want zero azimuth to have north to top + * (world -X -> tile +Y) and east to right (world -Z to tile +X), with height being up (world +Y -> tile +Z) + */ + Matrix3D transform = new Matrix3D(0.0, 0.0, -1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0); + /* Next, rotate world counterclockwise around Z axis by azumuth angle */ + transform.rotateXY(180-azimuth); + /* Next, rotate world by (90-inclination) degrees clockwise around +X axis */ + transform.rotateYZ(90.0-inclination); + /* Finally, shear along Z axis to normalize Z to be height above map plane */ + transform.shearZ(0, Math.tan(Math.toRadians(90.0-inclination))); + /* And scale Z to be same scale as world coordinates, and scale X and Y based on setting */ + transform.scale(scale, scale, Math.sin(Math.toRadians(inclination))); + world_to_map = transform; + /* Now, generate map to world tranform, by doing opposite actions in reverse order */ + transform = new Matrix3D(); + transform.scale(1.0/scale, 1.0/scale, 1/Math.sin(Math.toRadians(inclination))); + transform.shearZ(0, -Math.tan(Math.toRadians(90.0-inclination))); + transform.rotateYZ(-(90.0-inclination)); + transform.rotateXY(-180+azimuth); + Matrix3D coordswap = new Matrix3D(0.0, -1.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0); + transform.multiply(coordswap); + map_to_world = transform; + + Log.verboseinfo("Loading renderers for map '" + getClass().toString() + "'..."); + List renderers = configuration.createInstances("renderers", new Class[0], new Object[0]); + this.renderers = new HDMapTileRenderer[renderers.size()]; + renderers.toArray(this.renderers); + Log.verboseinfo("Loaded " + renderers.size() + " renderers for map '" + getClass().toString() + "'."); + } + + @Override + public MapTile[] getTiles(Location loc) { + DynmapWorld world = MapManager.mapman.getWorld(loc.getWorld().getName()); + HashSet tiles = new HashSet(); + Vector3D block = new Vector3D(); + block.setFromLocation(loc); /* Get coordinate for block */ + Vector3D corner = new Vector3D(); + /* Loop through corners of the cube */ + for(int i = 0; i < 2; i++) { + double inity = block.y; + for(int j = 0; j < 2; j++) { + double initz = block.z; + for(int k = 0; k < 2; k++) { + world_to_map.transform(block, corner); /* Get map coordinate of corner */ + addTile(tiles, world, (int)Math.floor(corner.x/tileWidth), (int)Math.floor(corner.y/tileHeight)); + + block.z += 1; + } + block.z = initz; + block.y += 1; + } + block.y = inity; + block.x += 1; + } + MapTile[] result = tiles.toArray(new MapTile[tiles.size()]); + Log.info("processed update for " + loc); + for(MapTile mt : result) + Log.info("need to render " + mt); + return result; + } + + @Override + public MapTile[] getAdjecentTiles(MapTile tile) { + HDMapTile t = (HDMapTile) tile; + DynmapWorld w = t.getDynmapWorld(); + int x = t.tx; + int y = t.ty; + return new MapTile[] { + new HDMapTile(w, this, t.renderer, x, y - 1), + new HDMapTile(w, this, t.renderer, x + 1, y), + new HDMapTile(w, this, t.renderer, x, y + 1), + new HDMapTile(w, this, t.renderer, x - 1, y) }; + } + + public void addTile(HashSet tiles, DynmapWorld world, int tx, int ty) { + for (int i = 0; i < renderers.length; i++) { + tiles.add(new HDMapTile(world, this, renderers[i], tx, ty)); + } + } + + public void invalidateTile(MapTile tile) { + } + + private static class Rectangle { + double r0x, r0z; /* Coord of corner of rectangle */ + double s1x, s1z; /* Side vector for one edge */ + double s2x, s2z; /* Side vector for other edge */ + public Rectangle(Vector3D v1, Vector3D v2, Vector3D v3) { + r0x = v1.x; + r0z = v1.z; + s1x = v2.x - v1.x; + s1z = v2.z - v1.z; + s2x = v3.x - v1.x; + s2z = v3.z - v1.z; + } + public Rectangle() { + } + public void setSquare(double rx, double rz, double s) { + this.r0x = rx; + this.r0z = rz; + this.s1x = s; + this.s1z = 0; + this.s2x = 0; + this.s2z = s; + } + double getX(int idx) { + return r0x + (((idx & 1) == 0)?0:s1x) + (((idx & 2) != 0)?0:s2x); + } + double getZ(int idx) { + return r0z + (((idx & 1) == 0)?0:s1z) + (((idx & 2) != 0)?0:s2z); + } + /** + * Test for overlap of projection of one vector on to anoter + */ + boolean testoverlap(double rx, double rz, double sx, double sz, Rectangle r) { + double rmin_dot_s0 = Double.MAX_VALUE; + double rmax_dot_s0 = Double.MIN_VALUE; + /* Project each point from rectangle on to vector: find lowest and highest */ + for(int i = 0; i < 4; i++) { + double r_x = r.getX(i) - rx; /* Get relative positon of second vector start to origin */ + double r_z = r.getZ(i) - rz; + double r_dot_s0 = r_x*sx + r_z*sz; /* Projection of start of vector */ + if(r_dot_s0 < rmin_dot_s0) rmin_dot_s0 = r_dot_s0; + if(r_dot_s0 > rmax_dot_s0) rmax_dot_s0 = r_dot_s0; + } + /* Compute dot products */ + double s0_dot_s0 = sx*sx + sz*sz; /* End of our side */ + if((rmax_dot_s0 < 0.0) || (rmin_dot_s0 > s0_dot_s0)) + return false; + else + return true; + } + /** + * Test if two rectangles intersect + * Based on separating axis theorem + */ + boolean testRectangleIntesectsRectangle(Rectangle r) { + /* Test if projection of each edge of one rectangle on to each edge of the other yields overlap */ + if(testoverlap(r0x, r0z, s1x, s1z, r) && testoverlap(r0x, r0z, s2x, s2z, r) && + testoverlap(r0x+s1x, r0z+s1z, s2x, s2z, r) && testoverlap(r0x+s2x, r0z+s2z, s1x, s1z, r) && + r.testoverlap(r.r0x, r.r0z, r.s1x, r.s1z, this) && r.testoverlap(r.r0x, r.r0z, r.s2x, r.s2z, this) && + r.testoverlap(r.r0x+r.s1x, r.r0z+r.s1z, r.s2x, r.s2z, this) && r.testoverlap(r.r0x+r.s2x, r.r0z+r.s2z, r.s1x, r.s1z, this)) { + return true; + } + else { + return false; + } + } + public String toString() { + return "{ " + r0x + "," + r0z + "}x{" + (r0x+s1x) + ","+ + (r0z+s1z) + "}x{" + (r0x+s2x) + "," + (r0z+s2z) + "}"; + } + } + + @Override + public List getRequiredChunks(MapTile tile) { + if (!(tile instanceof HDMapTile)) + return Collections.emptyList(); + + HDMapTile t = (HDMapTile) tile; + int min_chunk_x = Integer.MAX_VALUE; + int max_chunk_x = Integer.MIN_VALUE; + int min_chunk_z = Integer.MAX_VALUE; + int max_chunk_z = Integer.MIN_VALUE; + + /* Make corners for volume: 0 = bottom-lower-left, 1 = top-lower-left, 2=bottom-upper-left, 3=top-upper-left + * 4 = bottom-lower-right, 5 = top-lower-right, 6 = bottom-upper-right, 7 = top-upper-right */ + Vector3D corners[] = new Vector3D[8]; + int[] chunk_x = new int[8]; + int[] chunk_z = new int[8]; + for(int x = t.tx, idx = 0; x <= (t.tx+1); x++) { + for(int y = t.ty; y <= (t.ty+1); y++) { + for(int z = 0; z <= 1; z++) { + corners[idx] = new Vector3D(); + corners[idx].x = x*tileWidth; corners[idx].y = y*tileHeight; corners[idx].z = z*128; + map_to_world.transform(corners[idx]); + /* Compute chunk coordinates of corner */ + chunk_x[idx] = (int)Math.floor(corners[idx].x / 16); + chunk_z[idx] = (int)Math.floor(corners[idx].z / 16); + /* Compute min/max of chunk coordinates */ + if(min_chunk_x > chunk_x[idx]) min_chunk_x = chunk_x[idx]; + if(max_chunk_x < chunk_x[idx]) max_chunk_x = chunk_x[idx]; + if(min_chunk_z > chunk_z[idx]) min_chunk_z = chunk_z[idx]; + if(max_chunk_z < chunk_z[idx]) max_chunk_z = chunk_z[idx]; + idx++; + } + } + } + /* Make rectangles of X-Z projection of each side of the tile volume, 0 = top, 1 = bottom, 2 = left, 3 = right, + * 4 = upper, 5 = lower */ + Rectangle rect[] = new Rectangle[6]; + rect[0] = new Rectangle(corners[1], corners[3], corners[5]); + rect[1] = new Rectangle(corners[0], corners[2], corners[4]); + rect[2] = new Rectangle(corners[0], corners[1], corners[2]); + rect[3] = new Rectangle(corners[4], corners[5], corners[6]); + rect[4] = new Rectangle(corners[2], corners[3], corners[6]); + rect[5] = new Rectangle(corners[0], corners[1], corners[4]); + + /* Now, need to walk through the min/max range to see which chunks are actually needed */ + ArrayList chunks = new ArrayList(); + Rectangle chunkrect = new Rectangle(); + int misscnt = 0; + for(int x = min_chunk_x; x <= max_chunk_x; x++) { + for(int z = min_chunk_z; z <= max_chunk_z; z++) { + chunkrect.setSquare(x*16, z*16, 16); + boolean hit = false; + /* Check to see if square of chunk intersects any of our rectangle sides */ + for(int rctidx = 0; (!hit) && (rctidx < rect.length); rctidx++) { + if(chunkrect.testRectangleIntesectsRectangle(rect[rctidx])) { + hit = true; + } + } + if(hit) { + DynmapChunk chunk = new DynmapChunk(x, z); + chunks.add(chunk); + } + else { + misscnt++; + } + } + } + return chunks; + } + + @Override + public boolean render(MapChunkCache cache, MapTile tile, File outputFile) { + HDMapTile t = (HDMapTile) tile; + World w = t.getWorld(); + boolean rendered = false; + Color rslt = new Color(); + int[] pixel = new int[4]; + KzedBufferedImage im = KzedMap.allocateBufferedImage(tileWidth, tileHeight); + int[] argb_buf = im.argb_buf; + + MapIterator mapiter = cache.getIterator(0, 0, 0); + Vector3D top = new Vector3D(); + Vector3D bottom = new Vector3D(); + double xbase = t.tx * tileWidth; + double ybase = t.ty * tileHeight; + boolean odd = false; + for(int x = 0; x < tileWidth; x++) { + for(int y = 0; y < tileHeight; y++) { + top.x = bottom.x = xbase + x + 0.5; /* Start at center of pixel at Y=127.5, bottom at Y=-0.5 */ + top.y = bottom.y = ybase + y + 0.5; + top.z = 127.5; bottom.z = -0.5; + map_to_world.transform(top); /* Transform to world coordinates */ + map_to_world.transform(bottom); + raytrace(cache, mapiter, top, bottom, rslt, odd); + argb_buf[(tileHeight-y-1)*tileWidth + x] = rslt.getARGB(); + rendered = true; + odd = !odd; + } + odd = !odd; + } + /* Test to see if we're unchanged from older tile */ + TileHashManager hashman = MapManager.mapman.hashman; + long crc = hashman.calculateTileHash(argb_buf); + boolean tile_update = false; + FileLockManager.getWriteLock(outputFile); + try { + if((!outputFile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(), null, t.tx, t.ty))) { + /* Wrap buffer as buffered image */ + Debug.debug("saving image " + outputFile.getPath()); + if(!outputFile.getParentFile().exists()) + outputFile.getParentFile().mkdirs(); + try { + FileLockManager.imageIOWrite(im.buf_img, "png", outputFile); + } catch (IOException e) { + Debug.error("Failed to save image: " + outputFile.getPath(), e); + } catch (java.lang.NullPointerException e) { + Debug.error("Failed to save image (NullPointerException): " + outputFile.getPath(), e); + } + MapManager.mapman.pushUpdate(tile.getWorld(), new Client.Tile(tile.getFilename())); + hashman.updateHashCode(tile.getKey(), null, t.tx, t.ty, crc); + tile.getDynmapWorld().enqueueZoomOutUpdate(outputFile); + tile_update = true; + } + else { + Debug.debug("skipping image " + outputFile.getPath() + " - hash match"); + } + } finally { + FileLockManager.releaseWriteLock(outputFile); + KzedMap.freeBufferedImage(im); + } + MapManager.mapman.updateStatistics(tile, null, true, tile_update, !rendered); + return rendered; + } + + public enum BlockStep { + X_PLUS, + Y_PLUS, + Z_PLUS, + X_MINUS, + Y_MINUS, + Z_MINUS + }; + + /** + * Trace ray, based on "Voxel Tranversal along a 3D line" + */ + private void raytrace(MapChunkCache cache, MapIterator mapiter, Vector3D top, Vector3D bottom, Color rslt, boolean odd) { + /* Compute total delta on each axis */ + double dx = Math.abs(bottom.x - top.x); + double dy = Math.abs(bottom.y - top.y); + double dz = Math.abs(bottom.z - top.z); + /* Initial block coord */ + int x = (int) (Math.floor(top.x)); + int y = (int) (Math.floor(top.y)); + int z = (int) (Math.floor(top.z)); + /* Compute parametric step (dt) per step on each axis */ + double dt_dx = 1.0 / dx; + double dt_dy = 1.0 / dy; + double dt_dz = 1.0 / dz; + /* Initialize parametric value to 0 (and we're stepping towards 1) */ + double t = 0; + /* Compute number of steps and increments for each */ + int n = 1; + int x_inc, y_inc, z_inc; + + double t_next_y, t_next_x, t_next_z; + /* If perpendicular to X axis */ + if (dx == 0) { + x_inc = 0; + t_next_x = Double.MAX_VALUE; + } + /* If bottom is right of top */ + else if (bottom.x > top.x) { + x_inc = 1; + n += (int) (Math.floor(bottom.x)) - x; + t_next_x = (Math.floor(top.x) + 1 - top.x) * dt_dx; + } + /* Top is right of bottom */ + else { + x_inc = -1; + n += x - (int) (Math.floor(bottom.x)); + t_next_x = (top.x - Math.floor(top.x)) * dt_dx; + } + /* If perpendicular to Y axis */ + if (dy == 0) { + y_inc = 0; + t_next_y = Double.MAX_VALUE; + } + /* If bottom is above top */ + else if (bottom.y > top.y) { + y_inc = 1; + n += (int) (Math.floor(bottom.y)) - y; + t_next_y = (Math.floor(top.y) + 1 - top.y) * dt_dy; + } + /* If top is above bottom */ + else { + y_inc = -1; + n += y - (int) (Math.floor(bottom.y)); + t_next_y = (top.y - Math.floor(top.y)) * dt_dy; + } + /* If perpendicular to Z axis */ + if (dz == 0) { + z_inc = 0; + t_next_z = Double.MAX_VALUE; + } + /* If bottom right of top */ + else if (bottom.z > top.z) { + z_inc = 1; + n += (int) (Math.floor(bottom.z)) - z; + t_next_z = (Math.floor(top.z) + 1 - top.z) * dt_dz; + } + /* If bottom left of top */ + else { + z_inc = -1; + n += z - (int) (Math.floor(bottom.z)); + t_next_z = (top.z - Math.floor(top.z)) * dt_dz; + } + /* Walk through scene */ + rslt.setTransparent(); + BlockStep laststep = BlockStep.Y_MINUS; /* Last step is down into map */ + mapiter.initialize(x, y, z); + for (; n > 0; --n) { + int blocktype = mapiter.getBlockTypeID(); + if(blocktype != 0) { + Color[] clr = colorScheme.colors[blocktype]; + if(clr != null) { + if(laststep == BlockStep.Y_MINUS) + rslt.setColor(odd?clr[0]:clr[2]); + else if((laststep == BlockStep.X_PLUS) || (laststep == BlockStep.X_MINUS)) + rslt.setColor(clr[1]); + else + rslt.setColor(clr[3]); + } + return; + } + /* If X step is next best */ + if((t_next_x <= t_next_y) && (t_next_x <= t_next_z)) { + x += x_inc; + t = t_next_x; + t_next_x += dt_dx; + if(x_inc > 0) { + laststep = BlockStep.X_PLUS; + mapiter.incrementX(); + } + else { + laststep = BlockStep.X_MINUS; + mapiter.decrementX(); + } + } + /* If Y step is next best */ + else if((t_next_y <= t_next_x) && (t_next_y <= t_next_z)) { + y += y_inc; + t = t_next_y; + t_next_y += dt_dy; + if(y_inc > 0) { + laststep = BlockStep.Y_PLUS; + mapiter.incrementY(); + if(mapiter.getY() > 127) + return; + } + else { + laststep = BlockStep.Y_MINUS; + mapiter.decrementY(); + if(mapiter.getY() < 0) + return; + } + } + /* Else, Z step is next best */ + else { + z += z_inc; + t = t_next_z; + t_next_z += dt_dz; + if(z_inc > 0) { + laststep = BlockStep.Z_PLUS; + mapiter.incrementZ(); + } + else { + laststep = BlockStep.Z_MINUS; + mapiter.decrementZ(); + } + } + } + } + + @Override + public boolean isBiomeDataNeeded() { + return false; + } + + @Override + public boolean isRawBiomeDataNeeded() { + return false; + } + + @Override + public List baseZoomFilePrefixes() { + ArrayList s = new ArrayList(); + for(HDMapTileRenderer r : renderers) { + s.add(r.getName()); + } + return s; + } + + public int baseZoomFileStepSize() { return 1; } + + private static final int[] stepseq = { 3, 1, 2, 0 }; + + public int[] zoomFileStepSequence() { return stepseq; } + + /* How many bits of coordinate are shifted off to make big world directory name */ + public int getBigWorldShift() { return 5; } + + @Override + public String getName() { + return "HDMap"; + } + + @Override + public void buildClientConfiguration(JSONObject worldObject) { + for(HDMapTileRenderer renderer : renderers) { + renderer.buildClientConfiguration(worldObject); + } + } +} diff --git a/src/main/java/org/dynmap/hdmap/HDMapTile.java b/src/main/java/org/dynmap/hdmap/HDMapTile.java new file mode 100644 index 00000000..f3d5cc75 --- /dev/null +++ b/src/main/java/org/dynmap/hdmap/HDMapTile.java @@ -0,0 +1,58 @@ +package org.dynmap.hdmap; + +import org.dynmap.DynmapWorld; +import java.io.File; +import org.dynmap.MapTile; + +public class HDMapTile extends MapTile { + public HDMap map; + public HDMapTileRenderer renderer; + public int tx, ty; /* Tile X and Tile Y are in tile coordinates (pixels/tile-size) */ + private String fname; + + public HDMapTile(DynmapWorld world, HDMap map, HDMapTileRenderer renderer, int tx, int ty) { + super(world, map); + this.map = map; + this.renderer = renderer; + this.tx = tx; + this.ty = ty; + } + + @Override + public String getFilename() { + if(fname == null) { + fname = renderer.getName() + "/" + (tx >> 5) + '_' + (ty >> 5) + '/' + tx + "_" + ty + ".png"; + } + return fname; + } + + @Override + public String getDayFilename() { + return getFilename(); + } + + @Override + public int hashCode() { + return getFilename().hashCode() ^ getWorld().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof HDMapTile) { + return equals((HDMapTile) obj); + } + return super.equals(obj); + } + + public boolean equals(HDMapTile o) { + return o.tx == tx && o.ty == ty && o.renderer == o.renderer && o.getWorld().equals(getWorld()); + } + + public String getKey() { + return getWorld().getName() + "." + renderer.getName(); + } + + public String toString() { + return getWorld().getName() + ":" + getFilename(); + } +} diff --git a/src/main/java/org/dynmap/hdmap/HDMapTileRenderer.java b/src/main/java/org/dynmap/hdmap/HDMapTileRenderer.java new file mode 100644 index 00000000..0957c51b --- /dev/null +++ b/src/main/java/org/dynmap/hdmap/HDMapTileRenderer.java @@ -0,0 +1,19 @@ +package org.dynmap.hdmap; + +import java.io.File; + +import org.dynmap.utils.MapChunkCache; + +import org.json.simple.JSONObject; + +public interface HDMapTileRenderer { + String getName(); + + boolean render(MapChunkCache cache, HDMapTile tile, File outputFile); + + void buildClientConfiguration(JSONObject worldObject); + + boolean isBiomeDataNeeded(); + boolean isRawBiomeDataNeeded(); + boolean isNightAndDayEnabled(); +} diff --git a/src/main/java/org/dynmap/utils/Matrix3D.java b/src/main/java/org/dynmap/utils/Matrix3D.java new file mode 100644 index 00000000..583f792c --- /dev/null +++ b/src/main/java/org/dynmap/utils/Matrix3D.java @@ -0,0 +1,122 @@ +package org.dynmap.utils; + +/** + * Basic 3D matrix math class - prevent dependency on Java 3D for this + */ +public class Matrix3D { + private double m11, m12, m13, m21, m22, m23, m31, m32, m33; + /** + * Construct identity matrix + */ + public Matrix3D() { + m11 = m22 = m33 = 1.0; + m12 = m13 = m21 = m23 = m31 = m32 = 0.0; + } + /** + * Construct matrix with given parms + * + * @param m11 - first cell of first row + * @param m12 - second cell of first row + * @param m13 - third cell of first row + * @param m21 - first cell of second row + * @param m22 - second cell of second row + * @param m23 - third cell of second row + * @param m31 - first cell of third row + * @param m32 - second cell of third row + * @param m33 - third cell of third row + */ + public Matrix3D(double m11, double m12, double m13, double m21, double m22, double m23, double m31, double m32, double m33) { + this.m11 = m11; this.m12 = m12; this.m13 = m13; + this.m21 = m21; this.m22 = m22; this.m23 = m23; + this.m31 = m31; this.m32 = m32; this.m33 = m33; + } + /** + * Multiply matrix by another matrix (this = mat * this), and store result in self + * @param mat + */ + public void multiply(Matrix3D mat) { + double new_m11 = mat.m11*m11 + mat.m12*m21 + mat.m13*m31; + double new_m12 = mat.m11*m12 + mat.m12*m22 + mat.m13*m32; + double new_m13 = mat.m11*m13 + mat.m12*m23 + mat.m13*m33; + double new_m21 = mat.m21*m11 + mat.m22*m21 + mat.m23*m31; + double new_m22 = mat.m21*m12 + mat.m22*m22 + mat.m23*m32; + double new_m23 = mat.m21*m13 + mat.m22*m23 + mat.m23*m33; + double new_m31 = mat.m31*m11 + mat.m32*m21 + mat.m33*m31; + double new_m32 = mat.m31*m12 + mat.m32*m22 + mat.m33*m32; + double new_m33 = mat.m31*m13 + mat.m32*m23 + mat.m33*m33; + m11 = new_m11; m12 = new_m12; m13 = new_m13; + m21 = new_m21; m22 = new_m22; m23 = new_m23; + m31 = new_m31; m32 = new_m32; m33 = new_m33; + } + /** + * Scale each coordinate by given values + * + * @param s1 + * @param s2 + * @param s3 + */ + public void scale(double s1, double s2, double s3) { + Matrix3D scalemat = new Matrix3D(s1, 0, 0, 0, s2, 0, 0, 0, s3); + multiply(scalemat); + } + /** + * Rotate XY clockwise around +Z axis + * @param rot_deg - degrees of rotation + */ + public void rotateXY(double rot_deg) { + double rot_rad = Math.toRadians(rot_deg); + double sin_rot = Math.sin(rot_rad); + double cos_rot = Math.cos(rot_rad); + Matrix3D rotmat = new Matrix3D(cos_rot, sin_rot, 0, -sin_rot, cos_rot, 0, 0, 0, 1); + multiply(rotmat); + } + /** + * Rotate YZ clockwise around +X axis + * @param rot_deg - degrees of rotation + */ + public void rotateYZ(double rot_deg) { + double rot_rad = Math.toRadians(rot_deg); + double sin_rot = Math.sin(rot_rad); + double cos_rot = Math.cos(rot_rad); + Matrix3D rotmat = new Matrix3D(1, 0, 0, 0, cos_rot, sin_rot, 0, -sin_rot, cos_rot); + multiply(rotmat); + } + /** + * Shear along Z axis by factor of X and Y + */ + public void shearZ(double x_fact, double y_fact) { + Matrix3D shearmat = new Matrix3D(1, 0, 0, 0, 1, 0, x_fact, y_fact, 1); + multiply(shearmat); + } + /** + * Transform a given vector using the matrix + */ + public final void transform(double[] v) { + double v1 = m11*v[0] + m12*v[1] + m13*v[2]; + double v2 = m21*v[0] + m22*v[1] + m23*v[2]; + double v3 = m31*v[0] + m32*v[1] + m33*v[2]; + v[0] = v1; v[1] = v2; v[2] = v3; + } + /** + * Transform a given vector using the matrix + */ + public final void transform(Vector3D v) { + double v1 = m11*v.x + m12*v.y + m13*v.z; + double v2 = m21*v.x + m22*v.y + m23*v.z; + double v3 = m31*v.x + m32*v.y + m33*v.z; + v.x = v1; v.y = v2; v.z = v3; + } + + /** + * Transform a given vector using the matrix - put result in provided output vector + */ + public final void transform(Vector3D v, Vector3D outv) { + outv.x = m11*v.x + m12*v.y + m13*v.z; + outv.y = m21*v.x + m22*v.y + m23*v.z; + outv.z = m31*v.x + m32*v.y + m33*v.z; + } + + public String toString() { + return "[ [" + m11 + " " + m12 + " " + m13 + "] [" + m21 + " " + m22 + " " + m23 + "] [" + m31 + " " + m32 + " " + m33 + "] ]"; + } +} diff --git a/src/main/java/org/dynmap/utils/Vector3D.java b/src/main/java/org/dynmap/utils/Vector3D.java new file mode 100644 index 00000000..1beebfcf --- /dev/null +++ b/src/main/java/org/dynmap/utils/Vector3D.java @@ -0,0 +1,16 @@ +package org.dynmap.utils; +import org.bukkit.Location; +/** + * Simple vector class + */ +public class Vector3D { + public double x, y, z; + + public Vector3D() { x = y = z = 0.0; } + + public void setFromLocation(Location l) { x = l.getX(); y = l.getY(); z = l.getZ(); } + + public String toString() { + return "{ " + x + ", " + y + ", " + z + " }"; + } +} diff --git a/web/index.html b/web/index.html index 4ad5a33e..a2d246f7 100644 --- a/web/index.html +++ b/web/index.html @@ -26,6 +26,7 @@ + diff --git a/web/js/hdmap.js b/web/js/hdmap.js new file mode 100644 index 00000000..09027303 --- /dev/null +++ b/web/js/hdmap.js @@ -0,0 +1,78 @@ +function HDProjection() {} +HDProjection.prototype = { + extrazoom: 0, + fromLatLngToPoint: function(latLng) { + return new google.maps.Point(latLng.lng()*config.tileWidth,latLng.lat()*config.tileHeight); + }, + fromPointToLatLng: function(point) { + return new google.maps.LatLng(point.y/config.tileHeight, point.x/config.tileWidth); + }, + fromWorldToLatLng: function(x, y, z) { + return new google.maps.LatLng(-z / config.tileWidth / (1 << this.extrazoom), x / config.tileHeight / (1 << this.extrazoom)); + } +}; + +function HDMapType(configuration) { + $.extend(this, configuration); } +HDMapType.prototype = $.extend(new DynMapType(), { + constructor: HDMapType, + projection: new HDProjection(), + tileSize: new google.maps.Size(128.0, 128.0), + minZoom: 0, + maxZoom: 3, + prefix: null, + getTile: function(coord, zoom, doc) { + var tileSize = 128; + var imgSize; + var tileName; + + var extrazoom = this.dynmap.world.extrazoomout; + if(zoom < extrazoom) { + var scale = 1 << (extrazoom-zoom); + var zprefix = "zzzzzzzzzzzz".substring(0, extrazoom-zoom); + tileName = this.prefix + '/' + ((scale*coord.x) >> 5) + '_' + ((-scale*coord.y) >> 5) + + '/' + zprefix + "_" + (scale*coord.x) + '_' + (-scale*coord.y) + '.png'; + imgSize = 128; + } + else { + tileName = this.prefix + '/' + (coord.x >> 5) + '_' + ((-coord.y) >> 5) + + '/' + coord.x + '_' + (-coord.y) + '.png'; + imgSize = Math.pow(2, 7+zoom-extrazoom); + } + var tile = $('
') + .addClass('tile') + .css({ + width: tileSize + 'px', + height: tileSize + 'px' + }); + var img = $('') + .attr('src', this.dynmap.getTileUrl(tileName)) + .error(function() { img.hide(); }) + .bind('load', function() { img.show(); }) + .css({ + width: imgSize +'px', + height: imgSize + 'px', + borderStyle: 'none' + }) + .hide() + .appendTo(tile); + this.dynmap.registerTile(this, tileName, img); + //this.dynmap.unregisterTile(this, tileName); + return tile.get(0); + }, + updateTileSize: function(zoom) { + var size; + var extrazoom = this.dynmap.world.extrazoomout; + this.projection.extrazoom = extrazoom; + this.maxZoom = 3 + extrazoom; + if (zoom <= extrazoom) { + size = 128; + } + else { + size = Math.pow(2, 7+zoom-extrazoom); + } + this.tileSize = new google.maps.Size(size, size); + } +}); + +maptypes.HDMapType = function(configuration) { return new HDMapType(configuration); }; \ No newline at end of file