package org.dynmap.hdmap;

import static org.dynmap.JSONUtils.s;

import org.dynmap.DynmapWorld;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.dynmap.Client;
import org.dynmap.Color;
import org.dynmap.ConfigurationNode;
import org.dynmap.DynmapChunk;
import org.dynmap.DynmapCore;
import org.dynmap.Log;
import org.dynmap.MapManager;
import org.dynmap.MapTile;
import org.dynmap.MapType;
import org.dynmap.MapTypeState;
import org.dynmap.markers.impl.MarkerAPIImpl;
import org.dynmap.renderer.DynmapBlockState;
import org.dynmap.renderer.RenderPatch;
import org.dynmap.renderer.RenderPatchFactory.SideVisible;
import org.dynmap.storage.MapStorage;
import org.dynmap.storage.MapStorageTile;
import org.dynmap.utils.BlockStep;
import org.dynmap.hdmap.TexturePack.BlockTransparency;
import org.dynmap.utils.DynmapBufferedImage;
import org.dynmap.utils.LightLevels;
import org.dynmap.utils.DynLongHashMap;
import org.dynmap.utils.MapChunkCache;
import org.dynmap.utils.MapIterator;
import org.dynmap.utils.Matrix3D;
import org.dynmap.utils.PatchDefinition;
import org.dynmap.utils.Polygon;
import org.dynmap.utils.TileFlags;
import org.dynmap.utils.Vector3D;
import org.json.simple.JSONObject;

public class IsoHDPerspective implements HDPerspective {
    private final String name;
    private final int hashcode;
    /* View angles */
    public final double azimuth;  /* Angle in degrees from looking north (0), east (90), south (180), or west (270) */
    public final double compassazimuth;	// Angle in degrees from looking north (0), east (90), for the compass (default same as azimuth)
    public final double inclination;  /* Angle in degrees from horizontal (0) to vertical (90) */
    public final double maxheight;
    public final double minheight;
    /* 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 final Matrix3D world_to_map;
    private final Matrix3D map_to_world;
    
    /* Scale for default tiles */
    private final int basemodscale;
    
    /* 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 int MAX_SCALE = 64;
    public static final int MIN_SCALE = 1;
    
    private boolean need_biomedata = false;
    private boolean need_rawbiomedata = false;

    private static final BlockStep [] semi_steps = { BlockStep.Y_PLUS, BlockStep.X_MINUS, BlockStep.X_PLUS, BlockStep.Z_MINUS, BlockStep.Z_PLUS };
    
    // Cache for custom meshes by state (shared, reusable)
    private static RenderPatch[][] custom_meshes_by_globalstateindex = null;

    private class OurPerspectiveState implements HDPerspectiveState {
        DynmapBlockState blocktype = DynmapBlockState.AIR;
        DynmapBlockState lastblocktype = DynmapBlockState.AIR;
        Vector3D top, bottom, direction;
        int px, py;
        BlockStep laststep = BlockStep.Y_MINUS;
        
        BlockStep stepx, stepy, stepz;

        /* Scaled models for non-cube blocks */
        private final HDScaledBlockModels scalemodels;
        private final int modscale;

        /* Section-level raytrace variables */
        int sx, sy, sz;
        double sdt_dx, sdt_dy, sdt_dz;
        double st_next_x, st_next_y, st_next_z;
        /* Raytrace state variables */
        double dx, dy, dz;
        int x, y, z;
        double dt_dx, dt_dy, dt_dz, t;
        int n;
        int x_inc, y_inc, z_inc;        
        double t_next_y, t_next_x, t_next_z;
        boolean nonairhit;
        /* Subblock tracer state */
        int mx, my, mz;
        double xx, yy, zz;
        double mdt_dx;
        double mdt_dy;
        double mdt_dz;
        double togo;
        double mt_next_x, mt_next_y, mt_next_z;
        int subalpha;
        double mt;
        double mtend;
        int mxout, myout, mzout;
        /* Patch state and work variables */
        Vector3D v0 = new Vector3D();
        Vector3D vS = new Vector3D();
        Vector3D d_cross_uv = new Vector3D();
        // Double max patch count to be safe for patches + water patches
        double patch_t[] = new double[2*HDBlockModels.getMaxPatchCount()];
        double patch_u[] = new double[2*HDBlockModels.getMaxPatchCount()];
        double patch_v[] = new double[2*HDBlockModels.getMaxPatchCount()];
        boolean patch_shade[] = new boolean[2*HDBlockModels.getMaxPatchCount()];
        BlockStep patch_step[] = new BlockStep[2*HDBlockModels.getMaxPatchCount()];
        int patch_id[] = new int[2*HDBlockModels.getMaxPatchCount()];
        int cur_patch = -1;
        double cur_patch_u;
        double cur_patch_v;
        double cur_patch_t;
        boolean cur_shade;
        
        int[] subblock_xyz = new int[3];
        final MapIterator mapiter;
        final boolean isnether;
        boolean skiptoair;
        final int worldheight;
        final LightLevels llcache[];
        
        /* Cache for custom model patch lists */
        private final DynLongHashMap custom_meshes;
        private final DynLongHashMap custom_fluid_meshes;

        public OurPerspectiveState(MapIterator mi, boolean isnether, int scaled) {
            mapiter = mi;
            this.isnether = isnether;
            worldheight = mapiter.getWorldHeight();
            llcache = new LightLevels[4];
            for(int i = 0; i < llcache.length; i++)
                llcache[i] = new LightLevels();
            custom_meshes = new DynLongHashMap(4096);
            custom_fluid_meshes = new DynLongHashMap(4096);
            modscale = basemodscale << scaled;
            scalemodels = HDBlockModels.getModelsForScale(basemodscale << scaled);
        }
        
        private final void updateSemitransparentLight(LightLevels ll) {
        	int emitted = 0, sky = 0;
        	for(int i = 0; i < semi_steps.length; i++) {
        		int emit_sky_light = mapiter.getBlockLight(semi_steps[i]);
        		if ((emit_sky_light >> 8) > emitted) emitted = (emit_sky_light >> 8);
        		if ((emit_sky_light & 0xF) > sky) sky = (emit_sky_light & 0xF);
        	}
        	ll.sky = sky;
        	ll.emitted = emitted;
        }
        /**
         * Update sky and emitted light 
         */
        private final void updateLightLevel(DynmapBlockState blk, LightLevels ll) {
            /* Look up transparency for current block */
            BlockTransparency bt = HDBlockStateTextureMap.getTransparency(blk);
            switch(bt) {
            	case TRANSPARENT:
            		ll.sky = mapiter.getBlockSkyLight();
            		ll.emitted = mapiter.getBlockEmittedLight();
            		break;
            	case OPAQUE:
        			if (HDBlockStateTextureMap.getTransparency(lastblocktype) != BlockTransparency.SEMITRANSPARENT) {
        				int emit_sky_light = mapiter.getBlockLight(laststep.opposite());
            		    ll.sky = emit_sky_light & 0xF;
            		    ll.emitted = emit_sky_light >> 8;
        			}
        			else {
                		mapiter.unstepPosition(laststep);  /* Back up to block we entered on */
                		updateSemitransparentLight(ll);
                		mapiter.stepPosition(laststep);
        			}
        			break;
            	case SEMITRANSPARENT:
            		updateSemitransparentLight(ll);
            		break;
        		default:
                    ll.sky = mapiter.getBlockSkyLight();
                    ll.emitted = mapiter.getBlockEmittedLight();
                    break;
            }
        }
        /**
         * Get light level - only available if shader requested it
         */
        @Override
        public final void getLightLevels(LightLevels ll) {
            updateLightLevel(blocktype, ll);
        }
        /**
         * Get sky light level - only available if shader requested it
         */
        @Override
        public final void getLightLevelsAtStep(BlockStep step, LightLevels ll) {
            if(((step == BlockStep.Y_MINUS) && (y == 0)) ||
                    ((step == BlockStep.Y_PLUS) && (y == worldheight))) {
                getLightLevels(ll);
                return;
            }
            BlockStep blast = laststep;
            mapiter.stepPosition(step);
            laststep = blast;
            updateLightLevel(mapiter.getBlockType(), ll);
            mapiter.unstepPosition(step);
            laststep = blast;
        }
        /**
         * Get current block type ID
         */
        @Override
        public final DynmapBlockState getBlockState() { return blocktype; }
        /**
         * Get direction of last block step
         */
        @Override
        public final BlockStep getLastBlockStep() { return laststep; }
        /**
         * Get perspective scale
         */
        @Override
        public final double getScale() { return modscale; }
        /**
         * Get start of current ray, in world coordinates
         */
        @Override
        public final Vector3D getRayStart() { return top; }
        /**
         * Get end of current ray, in world coordinates
         */
        @Override
        public final Vector3D getRayEnd() { return bottom; }
        /**
         * Get pixel X coordinate
         */
        public final int getPixelX() { return px; }
        /**
         * Get pixel Y coordinate
         */
        public final int getPixelY() { return py; }
        /**
         * Get map iterator
         */
        public final MapIterator getMapIterator() { return mapiter; }
        /**
         * Return submodel alpha value (-1 if no submodel rendered)
         */
        public int getSubmodelAlpha() {
            return subalpha;
        }
        /**
         * Initialize raytrace state variables
         */
        private void raytrace_init() {
            /* Compute total delta on each axis */
            dx = Math.abs(direction.x);
            dy = Math.abs(direction.y);
            dz = Math.abs(direction.z);
            /* Compute parametric step (dt) per step on each axis */
            dt_dx = 1.0 / dx;
            dt_dy = 1.0 / dy;
            dt_dz = 1.0 / dz;
            /* Initialize parametric value to 0 (and we're stepping towards 1) */
            t = 0;
            /* Compute number of steps and increments for each */
            n = 1;

            /* Initial section coord */
            sx = fastFloor(top.x/16.0);
            sy = fastFloor(top.y/16.0);
            sz = fastFloor(top.z/16.0);
            /* Compute parametric step (dt) per step on each axis */
            sdt_dx = 16.0 / dx;
            sdt_dy = 16.0 / dy;
            sdt_dz = 16.0 / dz;
            
            /* If perpendicular to X axis */
            if (dx == 0) {
                x_inc = 0;
                st_next_x = Double.MAX_VALUE;
                stepx = BlockStep.X_PLUS;
                mxout = modscale;
            }
            /* If bottom is right of top */
            else if (bottom.x > top.x) {
                x_inc = 1;
                n += fastFloor(bottom.x) - x;
                st_next_x = (fastFloor(top.x/16.0) + 1 - (top.x/16.0)) * sdt_dx;
                stepx = BlockStep.X_PLUS;
                mxout = modscale;
            }
            /* Top is right of bottom */
            else {
                x_inc = -1;
                n += x - fastFloor(bottom.x);
                st_next_x = ((top.x/16.0) - fastFloor(top.x/16.0)) * sdt_dx;
                stepx = BlockStep.X_MINUS;
                mxout = -1;
            }
            /* If perpendicular to Y axis */
            if (dy == 0) {
                y_inc = 0;
                st_next_y = Double.MAX_VALUE;
                stepy = BlockStep.Y_PLUS;
                myout = modscale;
            }
            /* If bottom is above top */
            else if (bottom.y > top.y) {
                y_inc = 1;
                n += fastFloor(bottom.y) - y;
                st_next_y = (fastFloor(top.y/16.0) + 1 - (top.y/16.0)) * sdt_dy;
                stepy = BlockStep.Y_PLUS;
                myout = modscale;
            }
            /* If top is above bottom */
            else {
                y_inc = -1;
                n += y - fastFloor(bottom.y);
                st_next_y = ((top.y/16.0) - fastFloor(top.y/16.0)) * sdt_dy;
                stepy = BlockStep.Y_MINUS;
                myout = -1;
            }
            /* If perpendicular to Z axis */
            if (dz == 0) {
                z_inc = 0;
                st_next_z = Double.MAX_VALUE;
                stepz = BlockStep.Z_PLUS;
                mzout = modscale;
            }
            /* If bottom right of top */
            else if (bottom.z > top.z) {
                z_inc = 1;
                n += fastFloor(bottom.z) - z;
                st_next_z = (fastFloor(top.z/16.0) + 1 - (top.z/16.0)) * sdt_dz;
                stepz = BlockStep.Z_PLUS;
                mzout = modscale;
            }
            /* If bottom left of top */
            else {
                z_inc = -1;
                n += z - fastFloor(bottom.z);
                st_next_z = ((top.z/16.0) - fastFloor(top.z/16.0)) * sdt_dz;
                stepz = BlockStep.Z_MINUS;
                mzout = -1;
            }
            /* Walk through scene */
            laststep = BlockStep.Y_MINUS; /* Last step is down into map */
            nonairhit = false;
            skiptoair = isnether;
        }

        private final boolean handleSubModel(short[] model, HDShaderState[] shaderstate, boolean[] shaderdone) {
            boolean firststep = true;
            
            while(!raytraceSubblock(model, firststep)) {
                boolean done = true;
                for(int i = 0; i < shaderstate.length; i++) {
                    if(!shaderdone[i])
                        shaderdone[i] = shaderstate[i].processBlock(this);
                    done = done && shaderdone[i];
                }
                /* If all are done, we're out */
                if(done)
                    return true;
                nonairhit = true;
                firststep = false;
            }
            // Set current block as last block for any incomplete shaders
            for(int i = 0; i < shaderstate.length; i++) {
                if(!shaderdone[i])
                    // Update with current block as last block
                    shaderstate[i].setLastBlockState(blocktype);
            }
            return false;
        }
        
        private final int handlePatch(PatchDefinition pd, int hitcnt) {
            /* Compute origin of patch */
            v0.x = (double)x + pd.x0;
            v0.y = (double)y + pd.y0;
            v0.z = (double)z + pd.z0;
            /* Compute cross product of direction and V vector */
            d_cross_uv.set(direction);
            d_cross_uv.crossProduct(pd.v);
            /* Compute determinant - inner product of this with U */
            double det = pd.u.innerProduct(d_cross_uv);
            /* If parallel to surface, no intercept */
            switch(pd.sidevis) {
                case TOP:
                case TOPFLIP:
                case TOPFLIPV:
                case TOPFLIPHV:
                    if (det < 0.000001) {
                        return hitcnt;
                    }
                    break;
                case BOTTOM:
                    if (det > -0.000001) {
                        return hitcnt;
                    }
                    break;
                case BOTH:
                case FLIP:
                    if((det > -0.000001) && (det < 0.000001)) {
                        return hitcnt;
                    }
                    break;
            }
            double inv_det = 1.0 / det; /* Calculate inverse determinant */
            /* Compute distance from patch to ray origin */
            vS.set(top);
            vS.subtract(v0);
            /* Compute u - slope times inner product of offset and cross product */
            double u = inv_det * vS.innerProduct(d_cross_uv);
            if ((u <= pd.umin) || (u >= pd.umax)) {
                return hitcnt;
            }
            /* Compute cross product of offset and U */
            vS.crossProduct(pd.u);
            /* Compute V using slope times inner product of direction and cross product */
            double v = inv_det * direction.innerProduct(vS);
            // Check constrains: v must be below line from (umin, vmax) to (umax, vmaxatumax)
            double urel = (u > pd.umin) ? ((u - pd.umin) / (pd.umax - pd.umin)) : 0.0; 
            double vmaxatu = pd.vmax + (pd.vmaxatumax - pd.vmax) * urel;
            // Check constrains: v must be above line from (umin, vmin) to (umax, vminatumax)
            double vminatu = pd.vmin + (pd.vminatumax - pd.vmin) * urel;
            if ((v <= vminatu) || (v >= vmaxatu)) {
                return hitcnt;
            }
            /* Compute parametric value of intercept */
            double t = inv_det * pd.v.innerProduct(vS);
            if (t > 0.000001) { /* We've got a hit */
                patch_t[hitcnt] = t;
                patch_u[hitcnt] = u;
                patch_v[hitcnt] = v;
                patch_shade[hitcnt] = pd.shade;
                patch_id[hitcnt] = pd.textureindex;
                if(det > 0) {
                    patch_step[hitcnt] = pd.step.opposite();
                    if (pd.sidevis == SideVisible.TOPFLIP) {
                        patch_u[hitcnt] = 1 - u;
                    }
                    else if (pd.sidevis == SideVisible.TOPFLIPV) {
                        patch_v[hitcnt] = 1 - v;                    	
                    }
                    else if (pd.sidevis == SideVisible.TOPFLIPHV) {
                        patch_u[hitcnt] = 1 - u;
                        patch_v[hitcnt] = 1 - v;                    	
                    }
                }
                else {
                    if (pd.sidevis == SideVisible.FLIP) {
                        patch_u[hitcnt] = 1 - u;
                    }
                    patch_step[hitcnt] = pd.step;
                }
                hitcnt++;
            }
            return hitcnt;
        }
        
        private final boolean handlePatches(RenderPatch[] patches, HDShaderState[] shaderstate, boolean[] shaderdone, DynmapBlockState fluidstate, RenderPatch[] fluidpatches) {
            int hitcnt = 0;
            int water_hit = Integer.MAX_VALUE; // hit index of first water hit
            /* Loop through patches : compute intercept values for each */
            for(int i = 0; i < patches.length; i++) {
                hitcnt = handlePatch((PatchDefinition)patches[i], hitcnt);
            }
            if ((fluidpatches != null) && (fluidpatches.length > 0)) {
                int prev_hitcnt = hitcnt;
                for(int i = 0; i < fluidpatches.length; i++) {
                    hitcnt = handlePatch((PatchDefinition)fluidpatches[i], hitcnt);
                }
                if (prev_hitcnt < hitcnt) { // At least one water hit?
                    water_hit = prev_hitcnt;    // Remember index
                }
            }
            /* If no hits, we're done */
            if(hitcnt == 0) {
                // Set current block as last block for any incomplete shaders
                for(int i = 0; i < shaderstate.length; i++) {
                    if(!shaderdone[i])
                        // Update with current block as last block
                        shaderstate[i].setLastBlockState(blocktype);
                }
                return false;
            }
            BlockStep old_laststep = laststep;  /* Save last step */
            DynmapBlockState cur_bt = blocktype;
            for(int i = 0; i < hitcnt; i++) {
                /* Find closest hit (lowest parametric value) */
                double best_t = Double.MAX_VALUE;
                int best_patch = 0;
                for(int j = 0; j < hitcnt; j++) {
                    if(patch_t[j] < best_t) {
                        best_patch = j;
                        best_t = patch_t[j];
                    }
                }
                cur_patch = patch_id[best_patch]; /* Mark this as current patch */
                cur_patch_u = patch_u[best_patch];
                cur_patch_v = patch_v[best_patch];
                cur_shade = patch_shade[best_patch];
                laststep = patch_step[best_patch];
                cur_patch_t = best_t;
                // If the water patch, switch to water state and patch index
                if (best_patch >= water_hit) {
                    blocktype = fluidstate;
                }
                /* Process the shaders */
                boolean done = true;
                for(int j = 0; j < shaderstate.length; j++) {
                    if(!shaderdone[j])
                        shaderdone[j] = shaderstate[j].processBlock(this);
                    done = done && shaderdone[j];
                }
                // If water, restore block type
                if (best_patch >= water_hit) {
                    blocktype = cur_bt;
                }
                cur_patch = -1;
                /* If all are done, we're out */
                if(done) {
                    laststep = old_laststep;
                    return true;
                }
                nonairhit = true;
                /* Now remove patch and repeat */
                patch_t[best_patch] = Double.MAX_VALUE;
            }
            laststep = old_laststep;

            // Set current block as last block for any incomplete shaders
            for(int i = 0; i < shaderstate.length; i++) {
                if(!shaderdone[i])
                    // Update with current block as last block
                    shaderstate[i].setLastBlockState(blocktype);
            }

            return false;
        }

        private RenderPatch[] getPatches(DynmapBlockState bt, boolean isFluid) {
            RenderPatch[] patches = scalemodels.getPatchModel(bt);
            /* If no patches, see if custom model */
            if (patches == null) {
                CustomBlockModel cbm = scalemodels.getCustomBlockModel(bt);
                if (cbm != null) {   /* If found, see if cached already */
                    if (isFluid) {
                        patches = this.getCustomFluidMesh();
                        if (patches == null) {
                            patches = cbm.getMeshForBlock(mapiter);
                            this.setCustomFluidMesh(patches);
                        }
                    }
                    // Else, if block state specific model
                    else if (cbm.isOnlyBlockStateSensitive()) {
                    	patches = this.getCustomMeshForState(bt);	// Look up mesh by state
                    	if (patches == null) {	// Miss, generate it
                            patches = cbm.getMeshForBlock(mapiter);
                            this.setCustomMeshForState(bt, patches);
                    	}
                    }
                    else {	// Else, block specific
                        patches = this.getCustomMesh();
                        if (patches == null) {
                            patches = cbm.getMeshForBlock(mapiter);
                            this.setCustomMesh(patches);
                        }
                    }
                }
            }
            return patches;
        }
        /**
         * Process visit of ray to block
         */
        private final boolean visit_block(HDShaderState[] shaderstate, boolean[] shaderdone) {
            lastblocktype = blocktype;
            blocktype = mapiter.getBlockType();
            if (skiptoair) {	/* If skipping until we see air */
                if (blocktype.isAir()) {	/* If air, we're done */
                	skiptoair = false;
                }
            }
            else if(nonairhit || blocktype.isNotAir()) {
                short[] model;
                RenderPatch[] patches = getPatches(blocktype, false);
                /* Look up to see if block is modelled */
                if(patches != null) {
                    RenderPatch[] fluidpatches = null;
                    // If so, check for waterlogged
                    DynmapBlockState fluidstate = blocktype.getLiquidState();
                    if (fluidstate != null) {
                        fluidpatches = getPatches(fluidstate, true);
                    }
                    return handlePatches(patches, shaderstate, shaderdone, fluidstate, fluidpatches);
                }
                else if ((model = scalemodels.getScaledModel(blocktype)) != null) {
                    return handleSubModel(model, shaderstate, shaderdone);
                }
                else {
                	boolean done = true;
                    subalpha = -1;
                    for(int i = 0; i < shaderstate.length; i++) {
                        if(!shaderdone[i]) {
                            shaderdone[i] = shaderstate[i].processBlock(this);
                            // Update with current block as last block
                            shaderstate[i].setLastBlockState(blocktype);
                        }
                        done = done && shaderdone[i];
                    }
                    if (done)
                    	return true;
                    nonairhit = true;
                }
            }
            return false;
        }
        
        /* Skip empty : return false if exited */
        private final boolean raytraceSkipEmpty(MapChunkCache cache) {
        	int minsy = cache.getWorld().minY >> 4;
            while(cache.isEmptySection(sx, sy, sz)) {
                /* If Y step is next best */
                if((st_next_y <= st_next_x) && (st_next_y <= st_next_z)) {
                    sy += y_inc;
                    t = st_next_y;
                    st_next_y += sdt_dy;
                    laststep = stepy;
                    if (sy < minsy)
                        return false;
                }
                /* If X step is next best */
                else if((st_next_x <= st_next_y) && (st_next_x <= st_next_z)) {
                    sx += x_inc;
                    t = st_next_x;
                    st_next_x += sdt_dx;
                    laststep = stepx;
                }
                /* Else, Z step is next best */
                else {
                    sz += z_inc;
                    t = st_next_z;
                    st_next_z += sdt_dz;
                    laststep = stepz;
                }
            }
            return true;
        }
        
        /**
         * Step block iterator: false if done
         */
        private final boolean raytraceStepIterator(int miny, int maxy) {
            /* If Y step is next best */
            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;
                laststep = stepy;
                mapiter.stepPosition(laststep);
                /* If outside 0-(height-1) range */
                if ((y < miny) || (y > maxy)) {
                    return false;
                }
            }
            /* If X step is next best */
            else 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;
                laststep = stepx;
                mapiter.stepPosition(laststep);
            }
            /* Else, Z step is next best */
            else {
                z += z_inc;
                t = t_next_z;
                t_next_z += dt_dz;
                laststep = stepz;
                mapiter.stepPosition(laststep);
            }
            return true;
        }
        
        /**
         * Trace ray, based on "Voxel Tranversal along a 3D line"
         */
        private final void raytrace(MapChunkCache cache, HDShaderState[] shaderstate, boolean[] shaderdone) {
        	int minY = cache.getWorld().minY;
        	int height = cache.getWorld().worldheight;
        	
            /* Initialize raytrace state variables */
            raytrace_init();

            /* Skip sections until we hit a non-empty one */
            if (!raytraceSkipEmpty(cache))
                return;
            
            raytrace_section_init();
            
            if (y < minY)
                return;
            
            mapiter.initialize(x, y, z);
            
            for (; n > 0; --n) {
        		if (visit_block(shaderstate, shaderdone)) {
                    return;
                }
        		if (!raytraceStepIterator(minY, height)) {
        		    return;
        		}
            }
        }

        private final void raytrace_section_init() {
            t = t - 0.000001;
            double xx = top.x + t * direction.x;
            double yy = top.y + t * direction.y;
            double zz = top.z + t * direction.z;
            x = fastFloor(xx);  
            y = fastFloor(yy);  
            z = fastFloor(zz);
            t_next_x = st_next_x;
            t_next_y = st_next_y;
            t_next_z = st_next_z;
            n = 1;
            if(t_next_x != Double.MAX_VALUE) {
                if(stepx == BlockStep.X_PLUS) {
                    t_next_x = t + (x + 1 - xx) * dt_dx;
                    n += fastFloor(bottom.x) - x;
                }
                else {
                    t_next_x = t + (xx - x) * dt_dx;
                    n += x - fastFloor(bottom.x);
                }
            }
            if(t_next_y != Double.MAX_VALUE) {
                if(stepy == BlockStep.Y_PLUS) {
                    t_next_y = t + (y + 1 - yy) * dt_dy;
                    n += fastFloor(bottom.y) - y;
                }
                else {
                    t_next_y = t + (yy - y) * dt_dy;
                    n += y - fastFloor(bottom.y);
                }
            }
            if(t_next_z != Double.MAX_VALUE) {
                if(stepz == BlockStep.Z_PLUS) {
                    t_next_z = t + (z + 1 - zz) * dt_dz;
                    n += fastFloor(bottom.z) - z;
                }
                else {
                    t_next_z = t + (zz - z) * dt_dz;
                    n += z - fastFloor(bottom.z);
                }
            }
        }

        private final boolean raytraceSubblock(short[] model, boolean firsttime) {
            if(firsttime) {
            	mt = t + 0.00000001;
            	xx = top.x + mt * direction.x;  
            	yy = top.y + mt * direction.y;  
            	zz = top.z + mt * direction.z;
            	mx = (int)((xx - fastFloor(xx)) * modscale);
            	my = (int)((yy - fastFloor(yy)) * modscale);
            	mz = (int)((zz - fastFloor(zz)) * modscale);
            	mdt_dx = dt_dx / modscale;
            	mdt_dy = dt_dy / modscale;
            	mdt_dz = dt_dz / modscale;
            	mt_next_x = t_next_x;
            	mt_next_y = t_next_y;
            	mt_next_z = t_next_z;
            	if(mt_next_x != Double.MAX_VALUE) {
            		togo = ((t_next_x - t) / mdt_dx);
            		mt_next_x = mt + (togo - fastFloor(togo)) * mdt_dx;
            	}
            	if(mt_next_y != Double.MAX_VALUE) {
            		togo = ((t_next_y - t) / mdt_dy);
            		mt_next_y = mt + (togo - fastFloor(togo)) * mdt_dy;
            	}
            	if(mt_next_z != Double.MAX_VALUE) {
            		togo = ((t_next_z - t) / mdt_dz);
            		mt_next_z = mt + (togo - fastFloor(togo)) * mdt_dz;
            	}
            	mtend = Math.min(t_next_x, Math.min(t_next_y, t_next_z));
            }
            subalpha = -1;
            boolean skip = !firsttime;	/* Skip first block on continue */
            while(mt <= mtend) {
            	if(!skip) {
            		try {
            			int blkalpha = model[modscale*modscale*my + modscale*mz + mx];
            			if(blkalpha > 0) {
            				subalpha = blkalpha;
            				return false;
            			}
            		} catch (ArrayIndexOutOfBoundsException aioobx) {	/* We're outside the model, so miss */
            			return true;
            		}
            	}
            	else {
            		skip = false;
            	}
        		
                /* If X step is next best */
                if((mt_next_x <= mt_next_y) && (mt_next_x <= mt_next_z)) {
                    mx += x_inc;
                    mt = mt_next_x;
                    mt_next_x += mdt_dx;
                    laststep = stepx;
                    if(mx == mxout) {
                        return true;
                    }
                }
                /* If Y step is next best */
                else if((mt_next_y <= mt_next_x) && (mt_next_y <= mt_next_z)) {
                    my += y_inc;
                    mt = mt_next_y;
                    mt_next_y += mdt_dy;
                    laststep = stepy;
                    if(my == myout) {
                        return true;
                    }
                }
                /* Else, Z step is next best */
                else {
                    mz += z_inc;
                    mt = mt_next_z;
                    mt_next_z += mdt_dz;
                    laststep = stepz;
                    if(mz == mzout) {
                        return true;
                    }
                }
            }
            return true;
        }
        
        public final int[] getSubblockCoord() {
            if(cur_patch >= 0) {    /* If patch hit */
                double tt = cur_patch_t;
                double xx = top.x + tt * direction.x;  
                double yy = top.y + tt * direction.y;  
                double zz = top.z + tt * direction.z;
                subblock_xyz[0] = (int)((xx - fastFloor(xx)) * modscale);
                subblock_xyz[1] = (int)((yy - fastFloor(yy)) * modscale);
                subblock_xyz[2] = (int)((zz - fastFloor(zz)) * modscale);
            }
            else if(subalpha < 0) {
                double tt = t + 0.0000001;
                double xx = top.x + tt * direction.x;  
                double yy = top.y + tt * direction.y;  
                double zz = top.z + tt * direction.z;
                subblock_xyz[0] = (int)((xx - fastFloor(xx)) * modscale);
                subblock_xyz[1] = (int)((yy - fastFloor(yy)) * modscale);
                subblock_xyz[2] = (int)((zz - fastFloor(zz)) * modscale);
            }
            else {
                subblock_xyz[0] = mx;
                subblock_xyz[1] = my;
                subblock_xyz[2] = mz;
            }
            return subblock_xyz;
        }
        
        // Is the hit on a cullable face?
        public final boolean isOnFace() {
            double tt;
            if(cur_patch >= 0) {    /* If patch hit */
                tt = cur_patch_t;
            }
            else if(subalpha < 0) {
                tt = t + 0.0000001;
            }
            else {  // Full blocks always on face
                return true;
            }
            double xx = top.x + tt * direction.x;  
            double yy = top.y + tt * direction.y;  
            double zz = top.z + tt * direction.z;
            double xoff = xx - fastFloor(xx);
            double yoff = yy - fastFloor(yy);
            double zoff = zz - fastFloor(zz);
            return ((xoff < 0.00001) || (xoff > 0.99999) || (yoff < 0.00001) || (yoff > 0.99999) || (zoff < 0.00001) || (zoff > 0.99999));
        }
        
        /**
         * Get current texture index
         */
        @Override
        public final int getTextureIndex() {
            return cur_patch;
        }

        /**
         * Get current U of patch intercept
         */
        @Override
        public final double getPatchU() {
            return cur_patch_u;
        }

        /**
         * Get current V of patch intercept
         */
        @Override
        public final double getPatchV() {
            return cur_patch_v;
        }

        /**
         * Get current patch noShadow setting (true = no shadows/lighting)
         */
        @Override
        public final boolean getShade() {
        	// Shade if shade set OR not patch
            return cur_shade || (cur_patch < 0);    /* If patch hit */
        }
/**
         * Light level cache
         * @param index of light level (0-3)
         */
        @Override
        public final LightLevels getCachedLightLevels(int idx) {
            return llcache[idx];
        }
        /**
         * Get custom mesh for block, if defined (null if not)
         */
        public final RenderPatch[] getCustomMesh() {
            long key = this.mapiter.getBlockKey();  /* Get key for current block */
            return (RenderPatch[])custom_meshes.get(key);
        }
        /**
         * Get custom mesh for block, if defined (null if not)
         */
        public final RenderPatch[] getCustomMeshForState(DynmapBlockState bs) {
            return (RenderPatch[]) custom_meshes_by_globalstateindex[bs.globalStateIndex];
        }
        /**
         * Save custom mesh for block
         */
        public final void setCustomMesh(RenderPatch[] mesh) {
            long key = this.mapiter.getBlockKey();  /* Get key for current block */
            custom_meshes.put(key,  mesh);
        }
        /**
         * Set custom mesh for block, if defined (null if not)
         */
        public final void setCustomMeshForState(DynmapBlockState bs, RenderPatch[] mesh) {
            custom_meshes_by_globalstateindex[bs.globalStateIndex] = mesh;
        }
        /**
         * Get custom fluid mesh for block, if defined (null if not)
         */
        public final RenderPatch[] getCustomFluidMesh() {
            long key = this.mapiter.getBlockKey();  /* Get key for current block */
            return (RenderPatch[])custom_fluid_meshes.get(key);
        }
        /**
         * Save custom mesh for block
         */
        public final void setCustomFluidMesh(RenderPatch[] mesh) {
            long key = this.mapiter.getBlockKey();  /* Get key for current block */
            custom_fluid_meshes.put(key,  mesh);
        }
    }
    
    public IsoHDPerspective(DynmapCore core, ConfigurationNode configuration) {
    	if (custom_meshes_by_globalstateindex == null) {
    		custom_meshes_by_globalstateindex = new RenderPatch[DynmapBlockState.getGlobalIndexMax()][];
    	}
        name = configuration.getString("name", null);
        if(name == null) {
            Log.severe("Perspective definition missing name - must be defined and unique");
            hashcode = 0;
        }
        else {
            hashcode = name.hashCode();
        }
        double az = 90.0 + configuration.getDouble("azimuth", 135.0);    /* Get azimuth (default to classic kzed POV) */
        if (az >= 360.0) {
            az = az - 360.0;
        }
        azimuth = az;
        // Get compass azimuth - default to same as true azimuth, but allows for override
        az = 90.0 + configuration.getDouble("compassazimuth", az - 90.0);    /* Get azimuth (default to classic kzed POV) */
        if (az >= 360.0) {
            az = az - 360.0;
        }
        compassazimuth = az;
        
        double inc;
        inc = configuration.getDouble("inclination", 60.0);
        if(inc > MAX_INCLINATION) inc = MAX_INCLINATION;
        if(inc < MIN_INCLINATION) inc = MIN_INCLINATION;
        inclination = inc;
        int mscale = (int)Math.ceil(configuration.getDouble("scale", MIN_SCALE));
        if(mscale < MIN_SCALE) mscale = MIN_SCALE;
        if(mscale > MAX_SCALE) mscale = MAX_SCALE;
        basemodscale = mscale;
        /* Get max and min height */
        maxheight = configuration.getInteger("maximumheight", Integer.MIN_VALUE);
        
        minheight = configuration.getInteger("minimumheight", Integer.MIN_VALUE);
        /* 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(basemodscale, basemodscale, 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/basemodscale, 1.0/basemodscale, 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;
    }   

    @Override
    public List<TileFlags.TileCoord> getTileCoords(DynmapWorld world, int x, int y, int z, int tilescale) {
        HashSet<TileFlags.TileCoord> tiles = new HashSet<TileFlags.TileCoord>();
        Vector3D block = new Vector3D();
        block.x = x;
        block.y = y;
        block.z = z;
        Vector3D corner = new Vector3D();
        int tileSize = 128 << tilescale;
        /* 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 */
                    tiles.add(new TileFlags.TileCoord(fastFloor(corner.x/tileSize), fastFloor(corner.y/tileSize)));
                    block.z += 1;
                }
                block.z = initz;
                block.y += 1;
            }
            block.y = inity;
            block.x += 1;
        }
        return new ArrayList<TileFlags.TileCoord>(tiles);
    }

    @Override
    public List<TileFlags.TileCoord> getTileCoords(DynmapWorld world, int minx, int miny, int minz, int maxx, int maxy, int maxz, int tilescale) {
        ArrayList<TileFlags.TileCoord> tiles = new ArrayList<TileFlags.TileCoord>();
        Vector3D blocks[] = new Vector3D[] { new Vector3D(), new Vector3D() };
        blocks[0].x = minx - 1;
        blocks[0].y = miny - 1;
        blocks[0].z = minz - 1;
        blocks[1].x = maxx + 1;
        blocks[1].y = maxy + 1;
        blocks[1].z = maxz + 1;
        
        Vector3D corner = new Vector3D();
        Vector3D tcorner = new Vector3D();
        int mintilex = Integer.MAX_VALUE;
        int maxtilex = Integer.MIN_VALUE;
        int mintiley = Integer.MAX_VALUE;
        int maxtiley = Integer.MIN_VALUE;
        int tileSize = 128 << tilescale;
        /* Loop through corners of the prism */
        for(int i = 0; i < 2; i++) {
            corner.x = blocks[i].x;
            for(int j = 0; j < 2; j++) {
                corner.y = blocks[j].y;
                for(int k = 0; k < 2; k++) {
                    corner.z = blocks[k].z;
                    world_to_map.transform(corner, tcorner);  /* Get map coordinate of corner */
                    int tx = fastFloor(tcorner.x/(tileSize << tilescale));
                    int ty = fastFloor(tcorner.y/(tileSize << tilescale));
                    if(mintilex > tx) mintilex = tx;
                    if(maxtilex < tx) maxtilex = tx;
                    if(mintiley > ty) mintiley = ty;
                    if(maxtiley < ty) maxtiley = ty;
                }
            }
        }
        /* Now, add the tiles for the ranges - not perfect, but it works (some extra tiles on corners possible) */
        for(int i = mintilex; i <= maxtilex; i++) {
            for(int j = mintiley-1; j <= maxtiley; j++) {
                tiles.add(new TileFlags.TileCoord(i, j));
            }
        }
        return tiles;
    }

    @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, x - 1, y - 1, t.boostzoom, t.tilescale),
            new HDMapTile(w, this, x + 1, y - 1, t.boostzoom, t.tilescale),
            new HDMapTile(w, this, x - 1, y + 1, t.boostzoom, t.tilescale),
            new HDMapTile(w, this, x + 1, y + 1, t.boostzoom, t.tilescale),
            new HDMapTile(w, this, x, y - 1, t.boostzoom, t.tilescale),
            new HDMapTile(w, this, x + 1, y, t.boostzoom, t.tilescale),
            new HDMapTile(w, this, x, y + 1, t.boostzoom, t.tilescale),
            new HDMapTile(w, this, x - 1, y, t.boostzoom, t.tilescale) };
    }

    private static final int corners_by_side[][] = {
        { 1, 3, 7, 5 }, // Top
        { 0, 2, 6, 4 }, // Bottom
        { 0, 1, 3, 2 }, // Left
        { 4, 5, 7, 6 }, // Right
        { 2, 3, 7, 6 }, // Upper
        { 0, 1, 5, 4 }  // Lower
    };

    @Override
    public List<DynmapChunk> 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;
        int tileSize = tile.getTileSize();
        
        /* Make corners for volume: 
         * 0 = bottom-lower-left (xyz), 
         * 1 = top-lower-left (xyZ), 
         * 2 = bottom-upper-left (xYz), 
         * 3 = top-upper-left (xYZ),
         * 4 = bottom-lower-right (Xyz), 
         * 5 = top-lower-right (XyZ), 
         * 6 = bottom-upper-right (XYz), 
         * 7 = top-upper-right (XYZ) */  
        Vector3D corners[] = new Vector3D[8];
        double dx = -basemodscale, dy = -basemodscale;    /* Add 1 block on each axis */
        for(int x = t.tx, idx = 0; x <= (t.tx+1); x++) {
            dy = -basemodscale;
            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*tileSize + dx;
                    corners[idx].y = y*tileSize + dy;
                    corners[idx].z = (z == 1) ? t.getDynmapWorld().worldheight : t.getDynmapWorld().minY;
                    map_to_world.transform(corners[idx]);
                    /* Compute chunk coordinates of corner */
                    int cx = fastFloor(corners[idx].x / 16);
                    int cz = fastFloor(corners[idx].z / 16);
                    /* Compute min/max of chunk coordinates */
                    if(min_chunk_x > cx) min_chunk_x = cx;
                    if(max_chunk_x < cx) max_chunk_x = cx;
                    if(min_chunk_z > cz) min_chunk_z = cz;
                    if(max_chunk_z < cz) max_chunk_z = cz;
                    idx++;
                }
                dy = basemodscale;
            }
            dx = basemodscale;
        }
        /* 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 */
        Polygon[] side = new Polygon[6];
        for (int sidenum = 0; sidenum < side.length; sidenum++) {
            side[sidenum] = new Polygon();
            for (int corner = 0; corner < corners_by_side[sidenum].length; corner++) {
                int cid = corners_by_side[sidenum][corner];
                side[sidenum].addVertex(corners[cid].x, corners[cid].z);
            }
        }
        /* Now, need to walk through the min/max range to see which chunks are actually needed */
        ArrayList<DynmapChunk> chunks = new ArrayList<DynmapChunk>();
        
        for(int x = min_chunk_x; x <= max_chunk_x; x++) {
            for(int z = min_chunk_z; z <= max_chunk_z; z++) {
                boolean hit = false;
                for (int sidenum = 0; (!hit) && (sidenum < side.length); sidenum++) {
                    if (side[sidenum].clip(16.0*x, 16.0*z, 16.0*(x+1), 16.0*(z+1)) != null) {
                        hit = true;
                    }
                }
                //xs += c;
                if(hit) {
                    DynmapChunk chunk = new DynmapChunk(x, z);
                    chunks.add(chunk);
                }
            }
        }
        return chunks;
    }

    @Override
    public boolean render(MapChunkCache cache, HDMapTile tile, String mapname) {
        final long startTimestamp = System.currentTimeMillis();
        Color rslt = new Color();
        MapIterator mapiter = cache.getIterator(0, 0, 0);
        DynmapWorld world = tile.getDynmapWorld();
        int tileSize = tile.getTileSize();
        int scaled = 0;
        if ((tile.boostzoom > 0) && MarkerAPIImpl.testTileForBoostMarkers(cache.getWorld(), this, tile.tx * tileSize, tile.ty * tileSize, tileSize)) {
            scaled = tile.boostzoom;
        }
        int sizescale = 1 << scaled;

        /* Build shader state object for each shader */
        HDShaderState[] shaderstate = MapManager.mapman.hdmapman.getShaderStateForTile(tile, cache, mapiter, mapname, sizescale * this.basemodscale);
        int numshaders = shaderstate.length;
        if(numshaders == 0)
            return false;
        /* Check if nether world */
        boolean isnether = world.isNether();
        /* Create buffered image for each */
        DynmapBufferedImage im[] = new DynmapBufferedImage[numshaders];
        DynmapBufferedImage dayim[] = new DynmapBufferedImage[numshaders];
        int[][] argb_buf = new int[numshaders][];
        int[][] day_argb_buf = new int[numshaders][];
        boolean isOpaque[] = new boolean[numshaders];
        int bgday[] = new int[numshaders];
        int bgnight[] = new int[numshaders];
        
        for(int i = 0; i < numshaders; i++) {
            HDLighting lighting = shaderstate[i].getLighting();
            im[i] = DynmapBufferedImage.allocateBufferedImage(tileSize * sizescale, tileSize * sizescale);
            argb_buf[i] = im[i].argb_buf;
            if(lighting.isNightAndDayEnabled()) {
                dayim[i] = DynmapBufferedImage.allocateBufferedImage(tileSize * sizescale, tileSize * sizescale);
                day_argb_buf[i] = dayim[i].argb_buf;
            }
            isOpaque[i] = !shaderstate[i].getMap().getImageFormat().getEncoding().hasAlpha;
            bgday[i] = shaderstate[i].getMap().getBackgroundARGBDay();
            bgnight[i] = shaderstate[i].getMap().getBackgroundARGBNight();
        }
        // Mark the tiles we're going to render as validated
        for (int i = 0; i < numshaders; i++) {
            MapTypeState mts = world.getMapState(shaderstate[i].getMap());
            if (mts != null && mts.type.isReadOnly() == false) {
                mts.validateTile(tile.tx, tile.ty);
            }
        }
        /* Create perspective state object */
        OurPerspectiveState ps = new OurPerspectiveState(mapiter, isnether, scaled);        
        
        ps.top = new Vector3D();
        ps.bottom = new Vector3D();
        ps.direction = new Vector3D();
        double xbase = tile.tx * tileSize;
        double ybase = tile.ty * tileSize;
        boolean shaderdone[] = new boolean[numshaders];
        boolean rendered[] = new boolean[numshaders];
        double height = maxheight;
        if (height == Integer.MIN_VALUE) {    /* Not set - assume world height - 1 */
            if (isnether)
                height = 127;
            else
                height = tile.getDynmapWorld().worldheight - 1;
        }
        double miny = minheight;
        if (miny == Integer.MIN_VALUE) {    /* Not set - assume world height - 1 */
        	miny = tile.getDynmapWorld().minY;
        }
        
        for(int x = 0; x < tileSize * sizescale; x++) {
            ps.px = x;
            for(int y = 0; y < tileSize * sizescale; y++) {
                ps.top.x = ps.bottom.x = xbase + (x + 0.5) / sizescale;    /* Start at center of pixel at Y=height+0.5, bottom at Y=-0.5 */
                ps.top.y = ps.bottom.y = ybase + (y + 0.5) / sizescale;
                ps.top.z = height + 0.5; ps.bottom.z = miny - 0.5;
                map_to_world.transform(ps.top);            /* Transform to world coordinates */
                map_to_world.transform(ps.bottom);
                ps.direction.set(ps.bottom);
                ps.direction.subtract(ps.top);
                ps.py = y / sizescale;
                for(int i = 0; i < numshaders; i++) {
                    shaderstate[i].reset(ps);
                }
                try {
                    ps.raytrace(cache, shaderstate, shaderdone);
                } catch (Exception ex) {
                    Log.severe("Error while raytracing tile: perspective=" + this.name + ", coord=" + mapiter.getX() + "," + mapiter.getY() + "," + mapiter.getZ() + ", blockid=" + mapiter.getBlockType() + ", lighting=" + mapiter.getBlockSkyLight() + ":" + mapiter.getBlockEmittedLight() + ", biome=" + mapiter.getBiome().toString(), ex);
                    ex.printStackTrace();
                }
                for(int i = 0; i < numshaders; i++) {
                    if(shaderdone[i] == false) {
                        shaderstate[i].rayFinished(ps);
                    }
                    else {
                        shaderdone[i] = false;
                        rendered[i] = true;
                    }
                    shaderstate[i].getRayColor(rslt, 0);
                    int c_argb = rslt.getARGB();
                    if (c_argb != 0) rendered[i] = true;
                    if (isOpaque[i] && (c_argb == 0)) {
                        argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = bgnight[i];
                    }
                    else {
                        argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = c_argb;
                    }
                    if (day_argb_buf[i] != null) {
                        shaderstate[i].getRayColor(rslt, 1);
                        c_argb = rslt.getARGB();
                        if (isOpaque[i] && (c_argb == 0)) {
                            day_argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = bgday[i];
                        }
                        else {
                            day_argb_buf[i][(tileSize*sizescale-y-1)*tileSize*sizescale + x] = c_argb;
                        }
                    }
                }
            }
        }

        boolean renderone = false;
        /* Test to see if we're unchanged from older tile */
        MapStorage storage = world.getMapStorage();
        for(int i = 0; i < numshaders; i++) {
            long crc = MapStorage.calculateImageHashCode(argb_buf[i], 0, argb_buf[i].length);
            boolean tile_update = false;
            String prefix = shaderstate[i].getMap().getPrefix();

            MapStorageTile mtile = storage.getTile(world, shaderstate[i].getMap(), tile.tx, tile.ty, 0, MapType.ImageVariant.STANDARD);

            mtile.getWriteLock();
            try {
                if(mtile.matchesHashCode(crc) == false) {
                    /* Wrap buffer as buffered image */
                    if(rendered[i]) {   
                        mtile.write(crc, im[i].buf_img, startTimestamp);
                    }
                    else {
                        mtile.delete();
                    }
                    MapManager.mapman.pushUpdate(tile.getDynmapWorld(), new Client.Tile(mtile.getURI()));
                    tile_update = true;
                    renderone = true;
                }
                else {
                    if(!rendered[i]) {   
                        mtile.delete();
                    }
                }
            } finally {
                mtile.releaseWriteLock();
                DynmapBufferedImage.freeBufferedImage(im[i]);
            }
            MapManager.mapman.updateStatistics(tile, prefix, true, tile_update, !rendered[i]);
            /* Handle day image, if needed */
            if(dayim[i] != null) {
                crc = MapStorage.calculateImageHashCode(day_argb_buf[i], 0, day_argb_buf[i].length);

                mtile = storage.getTile(world, shaderstate[i].getMap(), tile.tx, tile.ty, 0, MapType.ImageVariant.DAY);

                mtile.getWriteLock();
                tile_update = false;
                try {
                    if(mtile.matchesHashCode(crc) == false) {
                        /* Wrap buffer as buffered image */
                        if(rendered[i]) {
                            mtile.write(crc, dayim[i].buf_img, startTimestamp);
                        }
                        else {
                            mtile.delete();
                        }
                        MapManager.mapman.pushUpdate(tile.getDynmapWorld(), new Client.Tile(mtile.getURI()));
                        tile_update = true;
                        renderone = true;
                    }
                    else {
                        if(!rendered[i]) {   
                            mtile.delete();
                        }
                    }
                } finally {
                    mtile.releaseWriteLock();
                    DynmapBufferedImage.freeBufferedImage(dayim[i]);
                }
                MapManager.mapman.updateStatistics(tile, prefix+"_day", true, tile_update, !rendered[i]);
            }
        }
        return renderone;
    }

    @Override
    public boolean isBiomeDataNeeded() {
        return need_biomedata;
    }

    @Override
    public boolean isRawBiomeDataNeeded() { 
         return need_rawbiomedata;
     }

    @Override
    public boolean isHightestBlockYDataNeeded() {
        return false;
    }
    
    @Override
    public boolean isBlockTypeDataNeeded() {
        return true;
    }
    
    @Override
    public double getScale() {
        return basemodscale;
    }

    @Override
    public int getModelScale() {
        return basemodscale;
    }

    @Override
    public String getName() {
        return name;
    }

    private static String[] directions = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
    @Override
    public void addClientConfiguration(JSONObject mapObject) {
        s(mapObject, "perspective", name);
        s(mapObject, "azimuth", azimuth);
        s(mapObject, "inclination", inclination);
        s(mapObject, "scale", basemodscale);
        s(mapObject, "worldtomap", world_to_map.toJSON());
        s(mapObject, "maptoworld", map_to_world.toJSON());
        int dir = (((360 + (int)(22.5+compassazimuth)) / 45) + 6) % 8;
        s(mapObject, "compassview", directions[dir]);
    }
    
    private static final int fastFloor(double f) {
        return ((int)(f + 1000000000.0)) - 1000000000;
    }
    
    @Override
    public void transformWorldToMapCoord(Vector3D input, Vector3D rslt) {
        world_to_map.transform(input,  rslt);
    }

    @Override
    public int hashCode() {
        return hashcode;
    }
}
