package usda.weru.gis.gui;

import java.awt.AlphaComposite;
import static java.awt.AlphaComposite.SRC_OVER;
import java.awt.Color;
import java.awt.Composite;
import org.locationtech.jts.geom.Coordinate;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.MapLayerListEvent;
import org.geotools.map.MapLayerListListener;
import org.geotools.map.MapViewport;
import org.geotools.ows.wmts.model.TileMatrixSet;
import org.geotools.ows.wmts.model.WMTSLayer;
import org.geotools.ows.wmts.model.WMTSServiceType;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.RenderListener;
import org.geotools.renderer.label.LabelCacheImpl;
import org.geotools.renderer.lite.LabelCache;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.tile.Tile;
import org.geotools.tile.TileService;
import org.geotools.tile.util.TileLayer;
import org.geotools.util.factory.Hints;
import org.geotools.renderer.lite.StreamingRenderer;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.openide.util.Exceptions;
import static usda.weru.gis.gui.MapViewerLayersContent.WepsLayerOpacityKey;
import usda.weru.gis.gui.MapViewerLayersContent.LayerDataContent;
import static usda.weru.gis.gui.MapViewerLayersContent.WepsCopyrightKey;
import usda.weru.gis.gui.geotools.ows.wmts.client.WMTSTileServiceWeps;
import usda.weru.util.ConfigData;

/**
 * handles user's changes to the map, such as zooming and panning
 * * @author Joseph A. Levin <joelevin@weru.ksu.edu>
 */
public class MapRenderLayer extends JComponent implements Runnable, MapController, MapLayerListListener, RenderListener {

    private static final long serialVersionUID = 1L;
    public static final String PROP_ZOOMFACTOR = "zoomFactor";
    public static final String PROP_PANOFFSET = "panOffset";
    
    MapViewerLayersContent layerContent;
    private StreamingRenderer renderer;
    boolean imageryWmsOsmOverZoomLimit;
    private final LabelCache c_labelCache;
    private Coordinate imageCenter;
    private static final double defaultZoomLevels[] = {5., 7., 12., 20., 100.,1000., 5000., 10000., 20000., 30000., 48000.};
    private double zoomLevels[];
    private int zoomIdx;
    private double zoomFactorDouble;
    private BufferedImage c_bufferImage;
    private double c_bufferFactor = .3d;
    private double c_bufferZoomFactor;
    private Coordinate c_bufferCenter;
    public static final Object RENDER_LOCK = new Object();
    private static final Object BUFFER_LOCK = new Object();
    private final ExecutorService c_renderService;
    private Future<?> c_renderTask;
    private boolean c_panning;
    private boolean renderEnabled;    
    private MapViewer c_viewer;
    private Map<String, Object> hints;    
    
    CoordinateReferenceSystem crsDefault;
    CoordinateReferenceSystem crsWms;
    Dimension imageSize;
    BufferedImage imageBufferMain;
    AffineTransform transform;
    Rectangle paintSize;
    ReferencedEnvelope envelopeDefault;
    ReferencedEnvelope envelopeImageryWms;
    Graphics2D graphicsMain;
    FontMetrics fontMetrics;
    Composite graphicsMainComposite;
    ArrayDeque<StreamingRenderer> rendererStack;
    private static final Object RENDERSTACK_LOCK = new Object();
   
    static int zoomFactorMin = 5;
    static int zoomFactorMax = 50000;
    static int zoomMin = 0;
    static int zoomMax = 100;

    public MapRenderLayer() {
        renderEnabled = false;
        addComponentListener(new InternalComponentListener());

        InternalPanAction panAction = new InternalPanAction();
        addMouseListener(panAction);
        addMouseMotionListener(panAction);

        InternalZoomAction zoomAction = new InternalZoomAction();
        addMouseWheelListener(zoomAction);

        c_renderService = Executors.newSingleThreadExecutor();

        imageCenter = new Coordinate(0, 0);
        setPreferredSize(new Dimension(500, 600));

        hints = new HashMap<>();
        //hints.put(StreamingRenderer.SCALE_COMPUTATION_METHOD_KEY, StreamingRenderer.SCALE_OGC);
        hints.put(StreamingRenderer.TEXT_RENDERING_KEY, StreamingRenderer.TEXT_RENDERING_STRING);
        // it looks like the 'optimized data loading' option no longer exists -max
        //hints.put(StreamingRenderer.OPTIMIZED_DATA_LOADING_KEY, true);
        hints.put(StreamingRenderer.ADVANCED_PROJECTION_HANDLING_KEY, true);
        hints.put("renderingBuffer", 20);
        c_labelCache = new LabelCacheImpl();
        hints.put(StreamingRenderer.LABEL_CACHE_KEY, c_labelCache);

        layerContent = null;
        renderer = null;
        imageSize = null;
        imageBufferMain = null;
        transform = null;
        paintSize = null;
        envelopeDefault = null;
        envelopeImageryWms = null;
        graphicsMain = null;
        graphicsMainComposite = null;
        
        zoomLevels = defaultZoomLevels;
        zoomIdx = 2;
                
        imageryWmsOsmOverZoomLimit = false;
        rendererStack = new ArrayDeque<>();
        
        try {
            Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE);
            CRSAuthorityFactory factory = ReferencingFactoryFinder.getCRSAuthorityFactory("EPSG", hints);
            //crsDefault = factory.createCoordinateReferenceSystem("EPSG:3857");
            crsDefault = factory.createCoordinateReferenceSystem("EPSG:4326");
            crsWms = factory.createCoordinateReferenceSystem("EPSG:4326");
        } catch (FactoryException ex) {
        }
        
        setRenderParms ();
    }

    public void setContent(MapViewerLayersContent content)  {
        layerContent = content;
        updateZoomFromLayers ();
    }
    
    public MapViewerLayersContent getContent()  {
        return layerContent;
    }
    
    @Override
    public void updateDataSources () {
        for (LayerDataContent lc : layerContent.layerData) {
            if (lc.service != null && lc.service instanceof WMTSTileServiceWeps) {
                ((WMTSTileServiceWeps)lc.service).initCacheDir();
            }
        }
    }
    
    @Override
    public void setCenter(Coordinate center) {
        boolean b = !(imageCenter.x != 0. || imageCenter.y != 0.);
        imageCenter = center;
        if (imageCenter.x != 0. || imageCenter.y != 0.) {
            setRenderParms();
            if (!c_panning) {
                preloadMapCache();
            }
            repaint();
        }
    }

    @Override
    public Coordinate getCenter() {
        return imageCenter;
    }
    
    @Override
    public void setZoomMax (int max) {
        zoomMax = max;
    }
    
    @Override
    public double getZoomFactor() {
        return zoomFactorDouble;
    }
    
    @Override
    public void setZoom(int idx) {
        zoomIdx = idx;
        zoomFactorDouble= zoomLevels[zoomIdx];
        setRenderParms();
        repaint();
    }

    @Override
    public void setZoomValues (double zoomValues[]) {
        int j=1;
        zoomLevels = zoomValues;
        c_viewer.setZoomLevels(zoomValues.length - 1);

        // this will force zoom to be set upon next zoom call
        zoomIdx = -1;
    }

    public Dimension getViewSize() {
        return getSize();
    }

    public void setBufferFactor(double factor) {
        if (factor < 0) {
            throw new IllegalArgumentException("Buffer factor must be equal or greater than 0.");
        }
        c_bufferFactor = factor;
        repaint();
    }

    @Override
    public void setViewer(MapViewer v) {
        c_viewer = v;
    }
    
    public Dimension calculateBufferImageSize() {
        Dimension view = getViewSize();

//        int width = (int) (view.width * (1 + c_bufferFactor));
//        int height = (int) (view.height * (1 + c_bufferFactor));
        int width = view.width;
        int height = view.height;

        return new Dimension(width, height);
    }

    private void paintImage(BufferedImage image, double zoom, Coordinate center) {
        synchronized (BUFFER_LOCK) {
            //todo: fire property change events if needed
            c_bufferImage = image;
            c_bufferZoomFactor = zoom;
            c_bufferCenter = center;
        }
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        if (c_bufferImage == null) {
            enqueueRender();
            return;
        }

        Graphics2D g2d = (Graphics2D) g;

        if (zoomFactorDouble != c_bufferZoomFactor || (!c_panning && !imageCenter.equals2D(c_bufferCenter))) {
            enqueueRender();
        }

        //make the temp zoom look pretty
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
        synchronized (BUFFER_LOCK) {
            //Scale the image while we wait for the new image to be rendered
            double zoom = zoomFactorDouble / c_bufferZoomFactor;

            //draw the buffer image zoomed and panned
            int panX = (int) ((c_bufferCenter.x - imageCenter.x) * zoomFactorDouble);
            int panY = (int) ((imageCenter.y - c_bufferCenter.y) * zoomFactorDouble);

            int width = (int) (c_bufferImage.getWidth() * zoom);
            int height = (int) (c_bufferImage.getHeight() * zoom);

            g2d.drawImage(c_bufferImage, (getWidth() / 2) - (width / 2) + panX,
                    (getHeight() / 2) - (height / 2) + panY, width, height, this);
        }
    }

    @Override
    public void setRenderEnabled(boolean enabled) {
        renderEnabled = enabled;
    }

    public void refresh () {
        stopRenderers();
        enqueueRender();
    }
    
    private void enqueueRender() {
        if (renderEnabled) {
            if (c_renderTask != null && !c_renderTask.isDone()) {
                c_renderTask.cancel(false);
                stopRenderers();
            }
            c_renderTask = c_renderService.submit(this);
        }
    }
    
    private void stopRenderers () {
        synchronized (RENDERSTACK_LOCK) {
            while (!rendererStack.isEmpty()) {
                StreamingRenderer r = rendererStack.pop();
                try {
                    r.stopRendering();
                } catch (Exception e) {
                    int j = 1;
                }
            }
        }
    }
    
    private void emptyRenderersStack () {
        synchronized (RENDERSTACK_LOCK) {
            while (!rendererStack.isEmpty()) {
                StreamingRenderer r = rendererStack.pop();
            }
        }
    }
    
    @Override
    public void run() {
        try {
            if(c_viewer != null) {
                c_viewer.setLoadingGraphicVisible(true);
            }
            
            render();
            
            if(c_viewer != null) {
                c_viewer.setLoadingGraphicVisible(false);
            }
        } catch (Exception e) {
            Logger.getLogger(MapRenderLayer.class).error("Render error:", e);
        }
    }
    
    public boolean setRenderParms () {
        boolean retVal = false;
        imageSize = calculateBufferImageSize();
        if (imageSize.width == 0 || imageSize.height == 0) {
            imageSize = null;
            return retVal;
        }
        if (imageCenter.x != 0. || imageCenter.y != 0.) {

            paintSize = new Rectangle(imageSize);
            imageBufferMain = new BufferedImage(imageSize.width, imageSize.height, BufferedImage.TYPE_INT_ARGB);
            graphicsMain = imageBufferMain.createGraphics();
            graphicsMain.setBackground(Color.white);
            graphicsMainComposite = graphicsMain.getComposite();
            graphicsMain.setFont(new Font(Font.SERIF, Font.PLAIN, 9));
            fontMetrics = graphicsMain.getFontMetrics();


            // create the transform
            transform = new AffineTransform();
            // Translate to the center of the buffer image.
            transform.translate((imageBufferMain.getWidth() / 2), (imageBufferMain.getHeight() / 2));
            // Scale with negative y factor to correct the orientation.
//            transform.scale(zoomFactor, -zoomFactor);
            transform.scale(zoomFactorDouble, -zoomFactorDouble);
            // Translate to the center of the feature collection.
            transform.translate(-imageCenter.x, -imageCenter.y);

            try {
                envelopeDefault = RendererUtilities.createMapEnvelope(
                        paintSize, transform, crsDefault);
                envelopeImageryWms = RendererUtilities.createMapEnvelope(
                        paintSize, transform, crsWms);
            } catch (NoninvertibleTransformException ex) {
                Exceptions.printStackTrace(ex);
            }
            retVal = true;
        }
        return retVal;
    }
    
    public void preloadMapCache () {
        
        if (layerContent.layerData.size() > 0) {
            // scale code from:
            //  org.geotools.tile.util/TileLayer.java
            int scale = 0;
            double resolution = 90.;
            ReferencedEnvelope newEnv = null;
            
            if (envelopeDefault == null || imageSize == null) {
                // bail
                return;
            }

            try {
                scale = (int) Math.round( RendererUtilities.calculateScale(
                                                envelopeDefault,
                                                imageSize.width,
                                                imageSize.height,
                                                resolution));

                int imageExtra = 300;
                int x = (paintSize.width+imageExtra+imageExtra)/2;
                int y = (paintSize.height+imageExtra+imageExtra)/2;
                AffineTransform transformTemp;
                // create the transform
                transformTemp = new AffineTransform();
                // Translate to the center of the buffer image.
                transformTemp.translate(x, y);
                // Scale with negative y factor to correct the orientation.
                transformTemp.scale(zoomFactorDouble, -zoomFactorDouble);
                // Translate to the center of the feature collection.
                transformTemp.translate(-imageCenter.x, -imageCenter.y);
                
                Rectangle newPaintSize = new Rectangle (x*2, y*2);
                newEnv = RendererUtilities.createMapEnvelope(
                        newPaintSize, transformTemp, crsDefault);
            } catch (FactoryException | TransformException ex) {
            } catch (NoninvertibleTransformException ex) {
            }
            
            for (LayerDataContent tileSrc : layerContent.layerData) {
                
                if (!tileSrc.layer.isVisible()) {
                    continue;
                }
            
                // Create a new instance of the TileService object
                // for this background thread so that its use
                // does not conflict with the current one
                //(Geotools does a check against concurrent usage)
                TileService newService = null;
                try {
                    TileService ser = tileSrc.service;
                    if (ser instanceof WMTSTileServiceWeps) {
                        newService = ser.getClass().
                            getConstructor(String.class,
                                           WMTSServiceType.class,
                                           WMTSLayer.class,
                                           String.class,
                                           TileMatrixSet.class).
                            newInstance(((WMTSTileServiceWeps) ser).getTemplateURL(),
                                        ((WMTSTileServiceWeps) ser).getType(),
                                        ((WMTSTileServiceWeps) ser).getLayer(),
                                        ((WMTSTileServiceWeps) ser).getStyleName(),
                                        ((WMTSTileServiceWeps) ser).getMatrixSet()
                                       );
                    }
                } catch (Exception ex) {
                    continue;
                }
                TileService service = newService;
                
                ReferencedEnvelope env = newEnv;
                int sc = scale;
                
                TileMatrixSet set = null;
                if (service instanceof WMTSTileServiceWeps) {
                    set = ((WMTSTileServiceWeps)service).getMatrixSet();
                }

                if (tileSrc.cacheLoadthread != null) {
                    tileSrc.cacheLoadthread.interrupt();
                }
                tileSrc.cacheLoadthread = new Thread (){
                    public void run() {
                        try {
                            //System.out.println("Tile cache preload starting");
                            int tileCnt = ConfigData.getIntParm(ConfigData.geotoolsTileExtentCount,512);
                            Collection<Tile> tiles = service.findTilesInExtent(env,  sc, false, tileCnt);
                            for (Tile t : tiles) {
                                if (!((WMTSTileServiceWeps)service).getCacheLoader().isTileCached(t)) {
                                    t.getBufferedImage();
                                }
                                //System.out.println("Tile cache loaded:"+t.getId());
                                if (interrupted()) {
                                    //System.out.println("+ Tile cache load interrupted");
                                    return;
                                }
                            }
                            //System.out.println("Tile cache preload complete");
                        } catch (Exception e){
                            // for interrupt
                            int j = 1;
                        }
                    }
                };
                tileSrc.cacheLoadthread.start();
            }
        }
    }
    
    // probably want to do this once viewer becomes visible
    // in order to improve response to user.
    public void haltpreloadMapCache () {
        if (layerContent.layerData.size() > 0) {
            for (LayerDataContent tileSrc : layerContent.layerData) {
                if (tileSrc.cacheLoadthread != null) {
                    tileSrc.cacheLoadthread.interrupt();
                }
            }
        }
    }
    
    private void render() {
        
        float opacity = 1;
        Composite opacitySave;
        String layerCopyrightStr = null;
        
        if (imageSize == null) {
            return;
        }

        if (layerContent.layerData.size() > 0) {
            synchronized (RENDER_LOCK) {
                c_labelCache.clear();
                    
                for (LayerDataContent tileSrc : layerContent.layerData) {
            try {
                    boolean b = tileSrc.layer.isSelected();
                    if (tileSrc.layer.isVisible()) {
                        renderer = new StreamingRenderer();
                        renderer.addRenderListener(this);
                        renderer.setRendererHints(hints);
                        synchronized (RENDERSTACK_LOCK) {
                            rendererStack.add(renderer);
                        }

                        if (tileSrc.layer instanceof TileLayer) {
                            MapViewport v = tileSrc.mapContent.getViewport();
                            v.setScreenArea(paintSize);
                            v.setCoordinateReferenceSystem(crsWms);
                            v.setBounds(envelopeDefault);
                        }
                        renderer.setMapContent(tileSrc.mapContent);

                        Map<String, Object> ud = tileSrc.layer.getUserData();
                        if (ud.containsKey(WepsLayerOpacityKey)) {
                            opacity = (float)ud.get(WepsLayerOpacityKey);
                        }
                        if (opacity < 1.) {
                            AlphaComposite newComp = AlphaComposite.getInstance(SRC_OVER, opacity);
                            graphicsMain.setComposite(newComp);
                        }
                        
                        renderer.paint(graphicsMain, paintSize, envelopeDefault, transform);

                        if (opacity < 1.) {
                            opacity = 1;
                            graphicsMain.setComposite(graphicsMainComposite);
                        }
                        
                        String cr = (String)tileSrc.layer.getUserData().get(WepsCopyrightKey);
                        layerCopyrightStr = (cr != null) ? cr : layerCopyrightStr;
                    }
            } catch (Exception ex) {
                int j=1;
            }
                }
                if (layerCopyrightStr != null) {
                    int fontW = fontMetrics.stringWidth(layerCopyrightStr);
                    int fontH = fontMetrics.getHeight();
                    int x = paintSize.width - fontW - 4;
                    int y = paintSize.height - 4;

                    graphicsMain.setComposite(AlphaComposite.getInstance(SRC_OVER, (float).6));
                    graphicsMain.setPaint(Color.LIGHT_GRAY);
                    graphicsMain.fillRect(x-4, y-fontH, fontW+8, fontH+4);
                    graphicsMain.setPaint(Color.BLACK);
                    graphicsMain.drawString(layerCopyrightStr, x, y);
                    graphicsMain.setComposite(graphicsMainComposite);
                }
            }
        }
        // All rendering complete at this point
        emptyRenderersStack();
        
        paintImage(imageBufferMain, zoomFactorDouble, new Coordinate(imageCenter.x, imageCenter.y));
    }

    private float getContentFloatParam (GTRenderer rend, String key, float defVal) {
        Object o = rend.getMapContent().getUserData().get(key);
        if (o != null) {
            defVal = (Float)o;
        }
        return defVal;
    }
    
    private Composite setOpacity (Graphics2D graphics, GTRenderer rend) {
        Composite comp = graphics.getComposite();
        float opacity = getContentFloatParam (rend, WepsLayerOpacityKey, (float)1.);
        if (opacity < 1.) {
            AlphaComposite newComp = AlphaComposite.getInstance(SRC_OVER, opacity);
            graphics.setComposite(newComp);
        }
        return comp;
    }
    
    private void resetOpacity (Graphics2D graphics, Composite comp) {
        graphics.setComposite(comp);
    }

    
    @Override
    public void zoomIn() {
        if(c_viewer != null) {
            c_viewer.zoomInc(1);
        }
    }

    @Override
    public void zoomOut() {
        if(c_viewer != null) {
            c_viewer.zoomInc(-1);
        }
    }

    @Override
    public void pan(int x, int y) {
        Coordinate center = getCenter();
        double zoom = zoomFactorDouble;
        double pX = x / zoom;
        double pY = y / zoom;
        Coordinate newCenter = new Coordinate(center.x - pX, center.y + pY);
        setCenter(newCenter);
    }

    @Override
    public boolean isPanning() {
        return c_panning;
    }

    @Override
    public void setPanning(boolean panning) {
        c_panning = panning;
    }

    /**
     * Handles setting the cursor high enough in the component tree so that every layer displays cursors.
     */
    @Override
    public void setCursor(Cursor cursor) {
        Container parent = SwingUtilities.getAncestorOfClass(MapPane.class, MapRenderLayer.this);
        if (parent == null) {
            parent = MapRenderLayer.this;
        }
        parent.setCursor(cursor);
    }

    @Override
    public void layerAdded(MapLayerListEvent event) {
        updateLayers ();
    }

    @Override
    public void layerRemoved(MapLayerListEvent event) {
        updateLayers ();
    }

    @Override
    public void layerChanged(MapLayerListEvent event) {
        updateLayers ();
    }

    @Override
    public void layerMoved(MapLayerListEvent event) {
        updateLayers ();
    }
    
    private void updateLayers () {
        updateZoomFromLayers ();
        graphicsMain.clearRect(0, 0, imageSize.width, imageSize.height);
        enqueueRender();
    }
    
    private void updateZoomFromLayers () {
        boolean wasSet = false;
        for (LayerDataContent tileSrc : layerContent.layerData) {
            if (tileSrc.layer.isVisible() && tileSrc.layer instanceof TileLayer) {
                setZoomValues (tileSrc.newScaleList);
                wasSet = true;
            }
        }
        if (!wasSet) {
            setZoomValues (defaultZoomLevels);
        }
    }

    @Override
    public int getZoomMin() {
        return zoomMin;
    }

    @Override
    public int getZoomMax() {
        return zoomMax;
    }

    @Override
    public void featureRenderer(SimpleFeature sf) {
        //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        String s = sf.getID();
        String s2 = sf.getName().toString();
        int j = 1;
    }

    @Override
    public void errorOccurred(Exception excptn) {
        //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        int j = 1;
    }

    private class InternalPanAction extends MouseAdapter {

        private int c_ix, c_iy;

        @Override
        public void mousePressed(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                c_panning = true;
                c_ix = e.getX();
                c_iy = e.getY();
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                setCursor(Cursor.getDefaultCursor());
                if (c_panning) {
                    c_panning = false;
                    preloadMapCache();
                }
                repaint();
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (c_panning) {
                setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
                int x = e.getX();
                int y = e.getY();
                int dx = x - c_ix;
                int dy = y - c_iy;
                pan(dx, dy);
                c_ix = x;
                c_iy = y;
            }
        }
    }

    private class InternalZoomAction extends MouseAdapter {
        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
            //zoom bar
            // we do in fact have a map pane
            int units = e.getUnitsToScroll();
            if (units < 0) {
                zoomIn();
            } else if (units > 0) {
                zoomOut();
            }
        }
    }

    private class InternalComponentListener extends ComponentAdapter {

        @Override
        public void componentResized(ComponentEvent e) {
            enqueueRender();
        }
    }

    @Override
    public void layerPreDispose(MapLayerListEvent arg0) {

    }

    @Override
    protected void processComponentEvent(ComponentEvent e) { 
        // Do our resize first
        int id = e.getID();
        switch(id) {
            case ComponentEvent.COMPONENT_RESIZED:
            setRenderParms();
            break;
        }
        
        // Now pass on
        super.processComponentEvent(e);
    }
}
