package com.algobase.share.maps;

import java.lang.Runnable;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.FileOutputStream;
import java.io.IOException;

import java.util.Date;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;

import java.text.SimpleDateFormat;

import android.os.Build;
import android.os.Handler;
import android.os.Environment;

import android.net.TrafficStats;

import android.content.Context;
import android.content.DialogInterface;

import android.util.AttributeSet;
import android.view.View;
import android.view.MotionEvent;
import android.view.Display;
import android.view.WindowManager;

import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Paint;
import android.graphics.Paint.Align;

import android.webkit.WebSettings;

import android.widget.Toast;

import android.location.Location;

import com.algobase.share.system.*;
import com.algobase.share.network.*;
import com.algobase.share.dialog.*;
import com.algobase.share.compat.*;


public class MyMapView extends View /* implements IMapView */
{
  public static interface IMapOverlay {
     public void setMapView(MyMapView map_view);
     public void draw(Canvas canvas);
     public void invalidate();
     public boolean isInvalid();
  }


  class TileStack {

     long[]   keys = null;
     Bitmap[] bitmaps = null;

     int first = 0;
     int count = 0;
     int sz = 0;

     int total_finds = 0;
     int successful_finds = 0;
   
     TileStack(int tile_num) { 
        sz = 2*tile_num; 
        keys = new long[sz];
        bitmaps = new Bitmap[sz];
        first = 0;
        count = 0;
        total_finds = 0;
        successful_finds = 0;
     }

     String getStatistics() {
       int unsuccessful_finds = total_finds - successful_finds;
       return format("%d/%d/%d", count, unsuccessful_finds, 
                                        successful_finds);
     }
   
     void clear() 
     { for(int i=0; i<bitmaps.length; i++) {
         if (bitmaps[i] != null) bitmaps[i].recycle();
         keys[i] = 0;
         bitmaps[i] = null;
       }
       first = 0;
       count = 0;
       total_finds = 0;
       successful_finds = 0;
     }

     void resize(int sz)
     {  clear();
        keys = new long[sz];
        bitmaps = new Bitmap[sz];
     }

     int size() { return count; }

     Bitmap find(long k)
     { total_finds++;
       Bitmap bmp = null;

       for(int i=0; i<bitmaps.length; i++) {
         int pos = (first + i) % bitmaps.length;
         if (bitmaps[pos] == null || keys[pos] == k) {
           bmp = bitmaps[pos];
           break;
         }
       }

       if (bmp != null) successful_finds++;
       return bmp;
     }
   
     void push(long k, Bitmap bmp) 
     { if (--first < 0) first = bitmaps.length-1;
       if (bitmaps[first] != null) bitmaps[first].recycle();
       keys[first] = k;
       bitmaps[first] = bmp;
       if (count < bitmaps.length) count++; 
     }

  }



  public boolean touchEventHandler(MotionEvent e) { return false; }

  public void writeLog(String txt)  {}
  public void showToast(String txt) {}
  public void setTitle(String txt)  {}
  public void setText1(String txt, int clr) {}
  public void setText2(String txt, int clr) {}

  public void dragStartHandler() {}
  public void dragEndHandler() {}

  public String centerLabel(double lat,double lon) { return null; }

  public MyProgressDialog newProgressDialog() { return null; }

  public boolean showProgress(int level, int n) { return true; }



  String format(String pattern, Object... args)
  { //return String.format(Locale.US,pattern,args);
    String s = "";
    try { s = String.format(Locale.US,pattern,args);
    } catch (Exception e) {}
    return s;
  }


  public static String[]  tileSourceNames = {
                               "OSM Road",
                               "OSM Terrain",
                               "OSM Light",
/*
                               "OSM Watercolor",
*/
                               "HERE Road",
                               "HERE Terrain",
                               "HERE Satellite",
                               "GMap Road",
                               "GMap Terrain",
                               "GMap Satellite",
                               //"XYZ Tiles"
                               };

   public static int[]  tileSourceMaxZoom = { 19, 17, 19,/*19,*/ // osm
                                              20, 20, 18,        // here
                                              20, 20, 20,        // gmap
                                              20 };
                               
   public static String[]  tileSourceUrls =  {

          "tile.openstreetmap.org/%d/%d/%d.png",
          "tile.opentopomap.org:443/%d/%d/%d.png",
          "basemaps.cartocdn.com/light_all/%d/%d/%d.png",
/*
          "a.ssl.fastly.net/watercolor/%d/%d/%d.png",
*/

          "https://maps.hereapi.com/v3/base/mc/%d/%d/%d/png?size=256&style=explore.day&lang=en",

          "https://maps.hereapi.com/v3/base/mc/%d/%d/%d/png?size=256&style=topo.day&lang=en",

          "https://maps.hereapi.com/v3/base/mc/%d/%d/%d/png?size=256&style=explore.satellite.day&lang=en",


          "https://tile.googleapis.com/v1/2dtiles/%d/%d/%d",
          "https://tile.googleapis.com/v1/2dtiles/%d/%d/%d",
          "https://tile.googleapis.com/v1/2dtiles/%d/%d/%d",

/*
          "https://mt%c.google.com/vt?lyrs=m&hl=de&z=%d&x=%d&y=%d",
          "https://mt%c.google.com/vt?lyrs=p&hl=de&z=%d&x=%d&y=%d",
          "https://mt%c.google.com/vt?lyrs=y&hl=de&z=%d&x=%d&y=%d",
*/

          "httpx://%c.tile.openstreetmap.org/%d/%d/%d.png"
    };


    //static final int MAX_ACTIVE_THREAD_NUM = 128;
    static final int MAX_ACTIVE_THREAD_NUM = 64;

    //static final int MAX_TILE_ERRORS = 10000;
    //static final int MAX_TOTAL_THREAD_NUM = 5000;

    static final int MAP_BG_COLOR = 0xffbbbbbb;


    int tile_errors = 0;

    MercatorProjection projection;

    Context context;

    String userAgent = null;

    String here_api_key;
    String gmap_api_key;

    String[] gmap_session_token = new String[3];


    //String tileProxyHost = "chomsky.uni-trier.de";
    //int    tileProxyPort = 9668;

    String tileProxyHost = null;
    int    tileProxyPort = 0;
  
    // animation

    float drag_off_x = 0;
    float drag_off_y = 0;

    int anim_step_num = 0;
    int anim_step = 0;

    double anim_lat_delta = 0;
    double anim_lon_delta = 0;

    float anim_xpix_delta = 0;
    float anim_ypix_delta = 0;

    float anim_zoom_f = 1.0f;
    float anim_zoom_delta = 0.0f;
    float anim_rotate_delta = 0.0f;
    int   anim_zoom_target = -1;

    Runnable animFinish = null;
    long animFinishDelay = 0;

    boolean keep_moving;

    boolean developerMode = false;

    Handler handler = new Handler();

    double buffer_center_x = 0;
    double buffer_center_y = 0;

    long buffer_tile_x = 0;
    long buffer_tile_y = 0;

    Bitmap bufferBitmap;
    Canvas bufferCanvas;
    Paint  paint;

    TileStack mapTileStack = null;
    TileStack radarTileStack = null;

    int corrupted_tiles = 0;

    IMapOverlay mapOverlay;

    AtomicInteger thread_active_count = new AtomicInteger(0);
    int thread_max_count = 0;
  //int thread_total_count = 0;

    double centerLon;
    double centerLat;

    float zoom_f = 1.0f;

    int tileSize = 512;
    int bufferTileSize = 512;

    int zoomLevel = 15;

    int minZoomLevel = 0;
    int maxZoomLevel = 18;

    int tile_cols;
    int tile_rows;

    int buffer_dx = 0;
    int buffer_dy = 0;

    int num_tiles = 0;
    int missing_tiles = 0;


    int viewWidth;
    int viewHeight;

    int mapWidth = -1;
    int mapHeight = -1;

    int bufWidth = -1;
    int bufHeight = -1;

    float down_x;
    float down_y;
    long  down_t;

    float move_x;
    float move_y;
    long  move_t;

    float move_speed_x;
    float move_speed_y;


    float rotation = 0;
    float heading = 0;

    Matrix rotationMatrix = new Matrix();

    File maps_folder;

    int cleanup_files = 0;
    int cleanup_folders = 0;

    String tileSource = "";
    String tileURL = "";
    String tilePath = "";

    String radarBaseURL = "https://tilecache.rainviewer.com/v2/radar/";

    int radarTileSize = 256;
/*
    clr = 1; //original
    clr = 2; //universal blue
    clr = 3; //titan
    clr = 4; //weather channel
    clr = 5; //meteored
*/
    int radarColor = 2; 
    int radarSmooth = 1;
    int radarSnow = 0; 

    boolean useDataConnection = true;
  
    boolean showRadar = false;

    String radar_time = "";
    long radar_sec = 0;
    int radar_step = 0;

    int radarAnimationMinutes = 120;
    int radarAnimationFrames = radarAnimationMinutes/10 + 1;

    Timer timer = new Timer();

/*
    void showToast(final String txt) {
     handler.post(new Runnable() {
        public void run() {
           Toast.makeText(context,txt, Toast.LENGTH_SHORT).show();
        }
     });
   }
*/


   void allocateBuffer(int cols, int rows)
   { 

     tile_cols = cols;
     tile_rows = rows;

     int w = bufferTileSize*tile_cols;
     int h = bufferTileSize*tile_rows;

     if (w == bufWidth && h == bufHeight) return;

   //showToast(format("buffer: %d x %d x %d",w,h,bufferTileSize));

     bufWidth = w;
     bufHeight = h;

     num_tiles = tile_cols * tile_rows;


     mapTileStack = new TileStack(num_tiles);
     radarTileStack = new TileStack(num_tiles);

     if (showRadar) num_tiles *= 2;

     if (bufferBitmap != null) bufferBitmap.recycle();

     //bufferBitmap = Bitmap.createBitmap(w,h,Bitmap.Config.ARGB_8888);

     // smaller and faster
     bufferBitmap = Bitmap.createBitmap(w,h,Bitmap.Config.RGB_565); 

     bufferCanvas = new Canvas(bufferBitmap);

     if (mapOverlay != null) mapOverlay.invalidate();

     invalidate();
   }



  public void anim_reset()
   { anim_step_num  = 0;
     anim_step = 0;
     drag_off_x = 0;
     drag_off_y = 0;
     anim_zoom_f = 1.0f;
     anim_zoom_delta = 0.0f;
     anim_rotate_delta = 0.0f;
     anim_zoom_target = zoomLevel;
   }


  public boolean animating()
  { return  (drag_off_x != 0 || drag_off_y != 0  || zoom_f != 1.0f || 
             anim_zoom_f != 1.0f || keep_moving || anim_step > 0); 
   }




// view

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    { super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      viewWidth = getMeasuredWidth();
      viewHeight = getMeasuredHeight();

      //int d = (int)Math.hypot(viewWidth,viewHeight);
      //setMeasuredDimension(d,d);

      setMeasuredDimension(viewWidth,viewHeight);
    }


/*
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
       float[] vec = { event.getX(), event.getY() };
       rotationMatrix.mapPoints(vec);
       event.setLocation(vec[0], vec[1]);
       return super.dispatchTouchEvent(event);
    }
*/



   @Override
   protected void onSizeChanged(int w, int h, int old_w, int old_h)
   { 
     if (w == 0 || h == 0) return;

     mapWidth = w;
     mapHeight= h;

/*
     tileSize = 256;
     if (mapWidth >  400) tileSize = 384;
     if (mapWidth >  600) tileSize = 512;
     if (mapWidth >  800) tileSize = 640;
     if (mapWidth > 1000) tileSize = 768;
     if (mapWidth > 1200) tileSize = 896;
     if (mapWidth > 1400) tileSize = 1024;
*/

     //int k = (int)(0.5f + (mapWidth-256)/128.0f);
     int k = (int)(mapWidth/128.0f + 0.5f) - 2;
     if (k < 2) k = 2;
     tileSize = k * 128;


     bufferTileSize = tileSize;

     projection.setTileSize(tileSize);

     if (mapWidth < 400)
       allocateBuffer(3,4);
     else
     if (mapWidth < 800)
       allocateBuffer(4,6);
     else
     if (mapWidth < 1000)
       allocateBuffer(5,7);
     else
       allocateBuffer(6,9);

     //paint.setTextSize(mapHeight/30);

     super.onSizeChanged(w,h,old_w,old_h);
    }


    void finishMoving()
    { shiftCenter(-drag_off_x,-drag_off_y);
      drag_off_x = 0;
      drag_off_y = 0;
      move_speed_x = 0;
      move_speed_y = 0;
      keep_moving = false;
    }


   @Override
   public boolean onTouchEvent(MotionEvent event)
   { 
     float[] vec = { event.getX(), event.getY() };
     rotationMatrix.mapPoints(vec);
     event.setLocation(vec[0], vec[1]);

     if (touchEventHandler(event)) return true;

     final float eventX = event.getX();
     final float eventY = event.getY();

     switch (event.getAction()) {

       case MotionEvent.ACTION_DOWN:
       { finishMoving();

         down_t = event.getEventTime();
         down_x = eventX;
         down_y = eventY;

         move_t = down_t;
         move_x = down_x;
         move_y = down_y;

         break;
       }


       case MotionEvent.ACTION_MOVE:
       {
         long t = event.getEventTime();

         float dx = eventX - move_x;
         float dy = eventY - move_y;
         float dt = t - move_t;

         // move_speed:  px/msec
         //if (dt > 0 && Math.hypot(dx,dy) > 5)
         if (dt > 0 && Math.hypot(dx,dy) > 1)
         { move_speed_x = dx/dt;
           move_speed_y = dy/dt;
           move_t = t;
           move_x = eventX;
           move_y = eventY;
          }

         if (drag_off_x == 0 && drag_off_y == 0) dragStartHandler();

         drag_off_x = eventX - down_x;
         drag_off_y = eventY - down_y;

         invalidate();

         break;
        }


       case MotionEvent.ACTION_UP:
       { 
         long t = event.getEventTime();

         float speed = (float)Math.hypot(move_speed_x,move_speed_y);
         float drag_dist = (float)Math.hypot(drag_off_x,drag_off_y);

         //showToast(format("speed %.2f px/msec",speed));

         //showToast(format("dist = %.1f speed = %.1f",drag_dist,speed));
         //showToast(format("speed: %.2f %.2f",move_speed_x,move_speed_y));


         float  max_speed = 7.5f;

         if (speed > max_speed) 
         { move_speed_x *= max_speed/speed;
           move_speed_y *= max_speed/speed;
          }

         if (speed < 0.5f || drag_dist < 10)
         { if (drag_off_x != 0 || drag_off_y != 0) dragEndHandler();
           shiftCenter(-drag_off_x,-drag_off_y);
           drag_off_x = 0;
           drag_off_y = 0;
           invalidate();
           break;
          }

         keep_moving = true;

         // 125 frames per second (one step: 8 msec)
         // 150 frames per second (one step: 6.7 msec)

         move_speed_x *= 7.0f;
         move_speed_y *= 7.0f;

         new MyThread() {
           public void run() { 
            int i = 0;
            while (keep_moving && Math.hypot(move_speed_x,move_speed_y) > 1)
            { drag_off_x += move_speed_x;
              drag_off_y += move_speed_y;
              move_speed_x *= 0.9875f;
              move_speed_y *= 0.9875f;
              postInvalidate();
              sleep(5);
              if (++i >= 250) break;
             }
            finishMoving();
            postInvalidate();
           }
        }.start();
   
        break;
      }

     }

     return true;
   }


  void drawBuffer(Canvas canvas, float x, float y) 
  {
    boolean bitmap_ok = true;

    try {
       x += buffer_dx;
       y += buffer_dy;
       canvas.drawBitmap(bufferBitmap,x,y,null);
/*
       int w = tile_cols * tileSize;
       int h = tile_rows * tileSize;
       int xx = (int)(x + 0.5f);
       int yy = (int)(y + 0.5f);
       Rect rect = new Rect(xx,yy,xx+w,yy+h);
       canvas.drawBitmap(bufferBitmap,null,rect,null);
*/
    } catch (Exception ex) { //showToast(ex.toString()); 
                             bitmap_ok = false; } 

    if (bitmap_ok) return;

    // bitmap too large ?
    // reduce tileSize
     
    int cols = tile_cols;
    int rows = tile_rows;
    tileSize -= 128;
    bufferTileSize = tileSize;

    showToast(format("Resize Buffer: %d x %d x %d", cols,rows,tileSize));

    projection.setTileSize(tileSize);
    allocateBuffer(cols,rows);
    buffer_tile_x = 0;
    buffer_tile_y = 0;
  }


  
  void update_status_line()
  {
    double z = zoomLevel;

    String label1 = tileSource;
    int label1_clr = 0xff303030;

    String label2 = "";
    int label2_clr = 0xff303030;
    if (showRadar) {
        label2 = " http://rainviewer.com ";
        label2_clr = 0xffc40000;
    }
    else 
    if (tileSource.startsWith("OSM"))  label2 = "\u00a9 OSM Contributors";
    else 
    if (tileSource.startsWith("GMap")) label2 = "LOGO:GOOGLE";
    else 
    if (tileSource.startsWith("HERE")) label2 = "LOGO:HERE";

    if (zoom_f != 1 ) {
       z += Math.log(zoom_f)/Math.log(2);
       label1 += format("  %.2f",z);
    }
    else 
    if (anim_zoom_f != 1) {
       z += Math.log(anim_zoom_f)/Math.log(2) - 0.01;
       label1 += format("  %.1f",z);
     }
    else
    if (showRadar) 
    { if (radar_step != 0) label1_clr = 0xffc40000;
      label1 = "Radar  " + radar_time;
     }
    else
    //if (developerMode) 
    if (false) 
    {
      label1 = format("%.1f   %d/%d",z,missing_tiles, num_tiles);
      label1 += format("    [%d|%d|%d]",thread_active_count.get(),
                                          thread_max_count,
                                          tile_errors);
      if(mapTileStack != null) 
        label1 += "    " + mapTileStack.getStatistics();

      label2 = "";
    }
    else
    if (missing_tiles > 0)
       label1 = format("Loading  %d / %d",missing_tiles, num_tiles);
    else 
      label1 += format("  %d ",(int)(z + 0.5));


     setText1(label1,label1_clr);
     setText2(label2,label2_clr);
  }



   @Override 
   protected void onDraw(final Canvas canvas) 
   {
     //int maxBitmapWidth = canvas.getMaximumBitmapWidth();
     //int maxBitmapHeight = canvas.getMaximumBitmapHeight();

     update_status_line();

     updateTileBuffer();

     float w = zoom_f*bufWidth;
     float h = zoom_f*bufHeight;

     float x = (mapWidth-w)/2 + drag_off_x;
     float y = (mapHeight-h)/2 + drag_off_y;

     canvas.save();
     canvas.rotate(-rotation,mapWidth/2,mapHeight/2);
     canvas.scale(zoom_f,zoom_f,x,y);
     drawBuffer(canvas,x,y); 
     canvas.restore();

     drawCrossHairs(canvas);

/*
     if (keep_moving) 
     { if ( Math.hypot(move_speed_x,move_speed_y) > 1)
       { drag_off_x += move_speed_x;
         drag_off_y += move_speed_y;
         move_speed_x *= 0.95f;
         move_speed_y *= 0.95f;
       }
       else 
        finishMoving();

      invalidate();
      return;
     }
*/

     if (missing_tiles > 0 && drag_off_x == 0 && drag_off_y == 0) {
       //showToast("keep on drawing");
       postInvalidateDelayed(250);
     }


     if (anim_step_num == 0) 
     { // no animation
       if (animFinish != null) 
       { //animFinish.run();
         //animFinish = null;

         final Runnable runnable = animFinish;
         animFinish = null;

         handler.postDelayed(new Runnable() {
           public void run() { runnable.run(); }
         },animFinishDelay);
       }

       return;
     }

     // zoom/pan/rotate animation

     w = anim_zoom_f*bufWidth;
     h = anim_zoom_f*bufHeight;
     x = (mapWidth-w)/2;
     y = (mapHeight-h)/2;

     if (anim_zoom_delta < 0)
     { x -= 0.5f*anim_step * anim_xpix_delta;
       y -= 0.5f*anim_step * anim_ypix_delta;
      }
     else
     if (anim_zoom_delta > 0)
     { x -= 2.0f*anim_step * anim_xpix_delta;
       y -= 2.0f*anim_step * anim_ypix_delta;
      }
     else
     { x -= anim_step * anim_xpix_delta;
       y -= anim_step * anim_ypix_delta;
      }



     canvas.save();
     canvas.rotate(-rotation,mapWidth/2,mapHeight/2);
/*
     canvas.rotate(-rotation,
                   mapWidth/2-(anim_step_num - anim_step)*anim_xpix_delta,
                   mapHeight/2-(anim_step_num - anim_step)*anim_ypix_delta);
*/
     canvas.scale(anim_zoom_f,anim_zoom_f,x,y);
     drawBuffer(canvas,x,y); 
     canvas.restore();

     drawCrossHairs(canvas);

     anim_zoom_f += anim_zoom_delta;

     if (anim_step > 0) rotation += anim_rotate_delta;

     anim_step++;

     //if (anim_step < anim_step_num) 
     if (anim_step <= anim_step_num) 
     { // ongoing animation
       invalidate(); 
       return;
      }

     // finish animation

     setZoomLevel(anim_zoom_target);
     rotationMatrix.setRotate(rotation,mapWidth/2,mapHeight/2);
     centerLat += anim_step_num*anim_lat_delta;
     centerLon += anim_step_num*anim_lon_delta;

     anim_reset();

     if (animFinish != null) 
     { //animFinish.run();
       //animFinish = null;

       final Runnable runnable = animFinish;
       animFinish = null;

       handler.postDelayed(new Runnable() {
         public void run() { runnable.run(); }
       },animFinishDelay);
     }

     // last redraw (delayed)
     postInvalidateDelayed(250);

  }



  void drawCrossHairs(Canvas canvas)
  { paint.setStyle(Paint.Style.STROKE);

    if (isSatellite())  
      paint.setColor(0xffffff80); // yellow
    else
      paint.setColor(0xff777777); // dark grey

    float xx = mapWidth/2;
    float yy = mapHeight/2;
    float r  = mapWidth/60;

    float lw = mapWidth/640.0f;

    canvas.save();
    canvas.translate(xx,yy);
    paint.setStrokeWidth(lw);
    canvas.drawCircle(0,0,r,paint);
    canvas.drawLine(-mapWidth,0,-r,0,paint);
    canvas.drawLine(r,0,mapWidth,0,paint);
    canvas.drawLine(0,-mapHeight,0,-r,paint);
    canvas.drawLine(0,r,0,mapHeight,paint);

    double center_x = projection.longitudeToPixelX(centerLon);
    double center_y = projection.latitudeToPixelY(centerLat);
    double lat = projection.pixelYToLatitude(center_y-drag_off_y);
    double lon = projection.pixelXToLongitude(center_x-drag_off_x);

    paint.setStyle(Paint.Style.FILL);
    paint.setTextAlign(Align.LEFT);

    if (isSatellite())  
      paint.setColor(Color.rgb(255,255,128));
    else
      paint.setColor(Color.BLACK);

    String s = centerLabel(lat,lon);

    if (s != null) 
    { r += 2;
      canvas.drawText(s,r,-r,paint);
     }

    canvas.restore();
   }



// tiles

   boolean downloadTile(String url, File tile_file)
   {
     if (url == null) return false;

     File tile_file_thr = new File(tile_file.getPath() + ".thr");

     if (url.startsWith("https") || tileProxyHost == null)
     { 
       if (!url.startsWith("https")) url = "https://" + url;

       HttpClient http = new HttpClient(url,5000);
       if (userAgent != null) http.setUserAgent(userAgent);

       if (http.getFile(tile_file_thr)) 
       { tile_file_thr.renameTo(tile_file);
         return true;
        }

       tile_errors++;
       tile_file_thr.delete();
       writeLog(url + "\n" + http.getError());
       return false;
     }

     // tileProxyHost != null and url does not start with "https"
     // use osm tile proxy server

     XClient xc = new XClient();
     xc.setHost(tileProxyHost);
     xc.setPort(tileProxyPort);

     String xpath = "/disk1/osm/" + url;

     if (xc.getFile(xpath,tile_file_thr))
     { tile_file_thr.renameTo(tile_file);
       xc.disconnect();
       return true;
      }

     tile_errors++;
     tile_file_thr.delete();
     writeLog(xpath);
     writeLog(format("Error(%d): ",tile_errors) + xc.getError());
     xc.disconnect();
     return false;
   }

   Bitmap loadTile(final String server_url, final String tile_folder,
                                            final int zoom, 
                                            final long x, 
                                            final long y,
                                            final TileStack tile_stack,
                                            final boolean radar)
   {
     if (x < 0 || x >= projection.tileSize()) return null;
     if (y < 0 || y >= projection.tileSize()) return null;

     final String png_name = format("%d.%d.png",x,y);
     final File zoom_folder = new File(tile_folder + "/" + zoom);
     final File tile_file = new File(zoom_folder, png_name);

     zoom_folder.mkdirs();

     long buf_key =  (y << 30) + x;
     if (radar) buf_key = -buf_key;

     Bitmap bmp = null;

   //if (tile_stack != null && tile_stack.size() >= num_tiles) 
     if (tile_stack != null && tile_stack.size() >= num_tiles/2) 
     { bmp = tile_stack.find(buf_key);
       if (bmp != null) return bmp;
     }

     long current_time = System.currentTimeMillis();

     // read tile file

     if (tile_file.exists())
     { 
/*
       BitmapFactory.Options options = new BitmapFactory.Options();
       options.inJustDecodeBounds = true;
       options.inPreferredConfig = Bitmap.Config.RGB_565;

       //writeLog("decode tile: \n" + tile_file.getPath());

       BitmapFactory.decodeFile(tile_file.getPath(),options);

       //if ((Build.VERSION.SDK_INT >= 26) {
       //writeLog("outConfig = " + options.outConfig);
       //writeLog("outMimeType = " + options.outMimeType);
       //}

       if (options.outWidth == 256 && options.outHeight == 256)
       { options.inJustDecodeBounds = false;
         bmp = BitmapFactory.decodeFile(tile_file.getPath(),options);
        }
*/

       bmp = BitmapFactory.decodeFile(tile_file.getPath());

       if (bmp == null) 
       { //delete corrupted file
         writeLog("Decode Failed\n" + tile_file.getPath());
         if (tile_file.lastModified() < current_time - 100) 
         { tile_file.delete();
           corrupted_tiles++;
          }
        }
     }

     if (bmp != null) {
       if (tile_stack != null) tile_stack.push(buf_key,bmp);
       return bmp;
     }

     // download tile (bmp = null)

     if (!useDataConnection) return null;

     if (thread_active_count.get() > MAX_ACTIVE_THREAD_NUM) {
        writeLog("Reached max active thread num.");
        return null;
     }


/*
     // work around for out-of-memory problem (ssl sockets not released)
     if (thread_total_count > MAX_TOTAL_THREAD_NUM) return null;
*/

/*

     if (tile_errors > MAX_TILE_ERRORS) {
        writeLog("Reached max tile errors.");
        return null;
      }
*/


/*
     // do not start a new download if another thread is currently 
     // downloading this tile (corresponding thr-file is existing)
     // On Android 11 this seem to block the main thread ?
*/


     final  File tile_file_thr = new File(tile_file.getPath() + ".thr");
     if (tile_file_thr.exists()) return null;


     //thread_total_count++;

     new MyThread() {
      public void run() {  

          try { tile_file_thr.createNewFile(); } catch(IOException e) {}

          int tc = thread_active_count.incrementAndGet();
          if (tc > thread_max_count) thread_max_count = tc;

          if (developerMode) update_status_line();

          String url = format(server_url,zoom,x,y);

          if (tileSource.startsWith("GMap")) {
            url += "?&key=" + gmap_api_key;
            if (tileSource == "GMap Road")
              url += "&session=" + gmap_session_token[0];
            if (tileSource == "GMap Terrain")
              url += "&session=" + gmap_session_token[1];
            if (tileSource == "GMap Satellite")
              url += "&session=" + gmap_session_token[2];

            if (url.endsWith("=")) url = null; // no session token
          }
          else
          if (tileSource.startsWith("HERE")) {
            url += "&apiKey=" + here_api_key;
          }


          if (!downloadTile(url,tile_file)) {
            //showToast("download failed: " + tile_file.getName());
            sleep(3000);
          }

          thread_active_count.decrementAndGet();
          if (developerMode) update_status_line();

        }

     }.start();

    return null;
  }



   Bitmap loadRadarTile(final int zoom, final long x, final long y)
   {
     final String radarURL = radarBaseURL + radar_sec + "/" + 
                                            radarTileSize + 
                                            "/%d/%d/%d/" + 
                                            radarColor + "/" + 
                                            radarSmooth + "_" + 
                                            radarSnow + ".png";

     String radar_path = maps_folder.getPath() + "/Radar/" + radar_sec;

     TileStack stack = (radar_step == 0) ? radarTileStack : null;
     return loadTile(radarURL,radar_path,zoom,x,y,stack,true);
   }


   void draw_buffer_tile(Canvas canvas, long tile_x, long tile_y, int i, int j)
   { 
     //writeLog(format("draw_buffer_tile: %d %d",i,j));


      if (i < 0 || i >= tile_cols || j < 0 || j >= tile_rows) return;

      int x = i*bufferTileSize;
      int y = j*bufferTileSize;

      long tile_xpos = tile_x + i;
      long tile_ypos = tile_y + j;

      if (tile_xpos < 0 || tile_xpos >= projection.tileSize() ||
          tile_ypos < 0 || tile_ypos >= projection.tileSize())
      { paint.setColor(MAP_BG_COLOR);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(x,y,x+tileSize,y+tileSize,paint);
        return;
      }


      Bitmap bmp = loadTile(tileURL,tilePath,zoomLevel,tile_xpos,tile_ypos,
                                                       mapTileStack,false);

      if (bmp != null) 
      { Rect rect = new Rect(x,y,x+bufferTileSize,y+bufferTileSize);
        canvas.drawBitmap(bmp,null,rect,null);
        if (mapTileStack == null) bmp.recycle();
       }
      else
      { missing_tiles++;

        paint.setColor(MAP_BG_COLOR);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(x,y,x+tileSize,y+tileSize,paint);

        paint.setColor(0xffdddddd);

        double lon = projection.tileXToLongitude(tile_x+i);
        double lat = projection.tileYToLatitude(tile_y+j);
        String txt = format("%d/%d/%d",zoomLevel,tile_x+i,tile_y+j);

        int d = tileSize/10;
        float xt = x+tileSize/2;
        float yt = y+tileSize/2 - d/2;
        canvas.drawText(tileSource,xt,yt-d,paint);
        canvas.drawText(txt,xt,yt,paint);
        canvas.drawText(format("%.6f",lat),xt,yt+d,paint);
        canvas.drawText(format("%.6f",lon),xt,yt+2*d,paint);

        paint.setStyle(Paint.Style.STROKE);
        canvas.drawRect(x,y,x+tileSize+1,y+tileSize+1,paint);

        if (developerMode && tile_errors > 0)
          canvas.drawText(format("[ %d ]",tile_errors),xt,yt+3*d,paint);
       }


      if (showRadar)
      { Bitmap bmp1 = loadRadarTile(zoomLevel,tile_xpos,tile_ypos);
        if (bmp1 !=null)
        { 
          Rect rect = new Rect(x,y,x+bufferTileSize,y+bufferTileSize);
          //paint.setAlpha(192);
          //canvas.drawBitmap(bmp1,null,rect,paint); // SLOW !!!!
          //paint.setAlpha(255);
          canvas.drawBitmap(bmp1,null,rect,null);
          if (radarTileStack == null || radar_step < 0) bmp1.recycle();
         }
        else 
        { missing_tiles++; 
          paint.setStyle(Paint.Style.FILL);
          paint.setColor(Color.RED);
          String txt = format("%d/%d",tile_x+i,tile_y+j);
          canvas.drawText(txt,x+tileSize/2,y+tileSize/2,paint);
         }
       }

    }



   public void updateTileBuffer()
   {
     double center_x = projection.longitudeToPixelX(centerLon);
     double center_y = projection.latitudeToPixelY(centerLat);

     long tile_x = projection.longitudeToTileX(centerLon) - (tile_cols/2);
     long tile_y = projection.latitudeToTileY(centerLat) - (tile_rows/2);

     if ((tile_cols % 2 == 0) && (center_x % tileSize > tileSize/2)) tile_x++;
     if ((tile_rows % 2 == 0) && (center_y % tileSize > tileSize/2)) tile_y++;

     int dx = (int)(tile_x*tileSize - (center_x - bufWidth/2));
     int dy = (int)(tile_y*tileSize - (center_y - bufHeight/2));

     buffer_dx = dx;
     buffer_dy = dy;


     boolean buffer_valid = missing_tiles == 0 && 
                            center_x == buffer_center_x && 
                            center_y == buffer_center_y && 
                            tile_x == buffer_tile_x     && 
                            tile_y == buffer_tile_y;

     boolean zooming = anim_zoom_delta > 0 || 
                       anim_zoom_f != 1.0f || 
                       zoom_f != 1.0f;

     boolean dragging = drag_off_x != 0 || drag_off_y != 0 || keep_moving;

     boolean overlay_valid = (mapOverlay == null || !mapOverlay.isInvalid());


     //if ((buffer_valid && overlay_valid) || dragging || anim_step > 0)
     //if ((buffer_valid && overlay_valid) || anim_step > 0)
     if ((buffer_valid && overlay_valid))
     { // do not update buffer
       update_status_line();
       return;
     }

     // buffer update required
/*
     showToast(format("buffer: %b / %b / %b", buffer_valid, overlay_valid,
                                              zooming_or_dragging));
*/

/*
     if (zoomLevel <= 3) bufferCanvas.drawColor(MAP_BG_COLOR);
*/

     bufferCanvas.drawColor(MAP_BG_COLOR);

     float lw = mapWidth/640.0f;
     paint.setStrokeWidth(lw);
     paint.setTextAlign(Align.CENTER);

     buffer_tile_x = tile_x;
     buffer_tile_y = tile_y;
     buffer_center_x = center_x;
     buffer_center_y = center_y;

     missing_tiles = 0;

     // fill buffer
     for(int i=0; i<tile_cols; i++) 
       for(int j=0; j<tile_rows; j++) 
         draw_buffer_tile(bufferCanvas,tile_x,tile_y,i,j);

/*
     // spiral order

     int x = tile_cols/2;
     int y = tile_rows/2;

     draw_buffer_tile(bufferCanvas,tile_x,tile_y,x,y);
     for(int d = 3; d <= tile_cols || d <= tile_rows; d += 2)
     { x -= 1;
       y -= 1;
       int d1 = d-1;
       for (int i=0; i<d1; i++) 
       { draw_buffer_tile(bufferCanvas,tile_x,tile_y,x+i,y);
         draw_buffer_tile(bufferCanvas,tile_x,tile_y,x+d1,y+i);
         draw_buffer_tile(bufferCanvas,tile_x,tile_y,x+d1-i,y+d1);
         draw_buffer_tile(bufferCanvas,tile_x,tile_y,x,y+d1-i);
        }
     }
*/


     if (mapOverlay != null) 
     { bufferCanvas.save();
       bufferCanvas.translate(-buffer_dx,-buffer_dy);
       mapOverlay.draw(bufferCanvas);
       bufferCanvas.restore();
     }

     update_status_line();
   }


   boolean downloadRadarTiles(int z_level, final MyProgressDialog progress)
   {
     String tileRadarPath = maps_folder.getPath() + "/Radar";

     double center_x = projection.longitudeToPixelX(centerLon);
     double center_y = projection.latitudeToPixelY(centerLat);

     long tile_x = projection.longitudeToTileX(centerLon) - (tile_cols/2);
     long tile_y = projection.latitudeToTileY(centerLat) - (tile_rows/2);

     long current_time = System.currentTimeMillis();
     long sec_end = current_time/1000;
     sec_end = 600*((sec_end-60)/600);
     Date date1 = new Date(1000*sec_end);

     long sec_start = sec_end - 600*(radarAnimationFrames-1);
     Date date0 = new Date(1000*sec_start);

     final String time_str0 = new SimpleDateFormat("HH:mm").format(date0);
     final String time_str1 = new SimpleDateFormat("HH:mm").format(date1);

     handler.post(new Runnable() {
        public void run() {
           progress.setMessage("Radar  " + time_str0 + " -- " + time_str1);
        }
     });

     progress.setMessage("Radar  " + time_str0 + " -- " + time_str1);


     int total_tiles = radarAnimationFrames*tile_cols*tile_rows;
     int downloaded_tiles = 0;
     int failed = 0;

     for(int t = 0; t<radarAnimationFrames; t++)
     { 
       long sec = sec_start + t*600;
/*
       final String time_str1 = 
            new SimpleDateFormat("HH:mm").format(new Date(1000*sec));

       handler.post(new Runnable() {
         public void run() { 
          progress.setMessage("Radar  " + time_str0 + " -- " + time_str1);
         }
       });
*/

       for(int i = 0; i<tile_cols; i++)
       { for(int j = 0; j<tile_rows; j++)
         { long x = tile_x + i;
           long y = tile_y + j;

           if (x < 0 || x >= projection.tileSize()) continue;
           if (y < 0 || y >= projection.tileSize()) continue;
  
           File tile_file = new File(tileRadarPath 
                                       + "/" + sec 
                                       + format("/%d/%d.%d.png",z_level,x,y));

           handler.post(new Runnable() {
             public void run() { progress.incrementProgressBy(1); }
           });

          if (!progress.isShowing()) 
          { showToast("Download canceled.");
            return false;
           }

           if (tile_file.exists()) continue;

           downloaded_tiles++;

           tile_file.getParentFile().mkdirs();

           String radarURL = radarBaseURL + sec + "/" + 
                                            radarTileSize + 
                                            "/%d/%d/%d/" + 
                                            radarColor + "/" + 
                                            radarSmooth + "_" + 
                                            radarSnow + ".png";

           String url = format(radarURL,z_level,x,y);


           HttpClient http = new HttpClient(url,1000);
           if (userAgent != null) http.setUserAgent(userAgent);

           if (!http.getFile(tile_file)) {
             failed++;
             tile_file.delete();
             writeLog("HTTP Error: " + http.getError());
           }
         }
       }

    }

    //showToast(format("Radar: %d / %d Tiles",downloaded_tiles,total_tiles));

   return true;
  }


   int missingRadarTiles(int z_level)
   {
     String tileRadarPath = maps_folder.getPath() + "/Radar";

     double center_x = projection.longitudeToPixelX(centerLon);
     double center_y = projection.latitudeToPixelY(centerLat);

     long tile_x = projection.longitudeToTileX(centerLon) - (tile_cols/2);
     long tile_y = projection.latitudeToTileY(centerLat) - (tile_rows/2);

     long current_time = System.currentTimeMillis();
     long sec_end = current_time/1000;
     sec_end = 600*((sec_end-60)/600);
     long sec_start = sec_end - 600*(radarAnimationFrames-1);

     int count = 0;

     for(int t = 0; t<radarAnimationFrames; t++)
     { long sec = sec_start + t*600;
       for(int i = 0; i<tile_cols; i++)
       { for(int j = 0; j<tile_rows; j++)
         { long x = tile_x + i;
           long y = tile_y + j;
           if (x < 0 || x >= projection.tileSize()) continue;
           if (y < 0 || y >= projection.tileSize()) continue;
           File tile_file = new File(tileRadarPath 
                                       + "/" + sec 
                                       + format("/%d/%d.%d.png",z_level,x,y));
           if (!tile_file.exists()) count++;
         }
       }

    }

    return count;
  }


  public void startRadarAnimation() 
  {
    //if (!showRadar) return;

    setShowRadar(true);

    radar_step = -radarAnimationFrames;

    update_status_line();

    final MyProgressDialog progress = newProgressDialog();
    progress.setBackgroundColor(0xffcccccc);
    progress.setProgressStyle(MyProgressDialog.PROGRESS_STYLE_HORIZONTAL);
    progress.setProgressNumberFormat("%d / %d  Tiles");
    int total_tiles = radarAnimationFrames*tile_cols*tile_rows;
    progress.setMax(total_tiles);
    progress.setProgress(0);

    progress.setOnCancelListener(new DialogInterface.OnCancelListener() {
       @Override
       public void onCancel(DialogInterface diag) {
            radar_step = 0;
            update_status_line();
            progress.dismiss();
          }
    });


    new MyThread() {
      public void run() { 

         boolean complete = false;

         if (missingRadarTiles(zoomLevel) == 0)
         { showToast("Tiles complete");
           complete = true;
          }
         else
         {
           progress.show(); 

           int msec = 0;
           while (msec < 500 && !progress.isShowing())
           { sleep(50);
             msec += 50;
            }
  
           if (!progress.isShowing()) {
             showToast("ProgressBar: open failed.");
           }
  
           complete = downloadRadarTiles(zoomLevel,progress); 
           sleep(1500);
           progress.dismiss();

         }

         if (!complete) {
           radar_step = 0;
           update_status_line();
           return;
         }

         handler.post(new Runnable() {
            public void run() {
               if (radar_step < 0)
               { setRadarStep(radar_step+1);
                 refresh();
                 //handler.postDelayed(this,1500);
                 handler.postDelayed(this,1000);
                }
            }
         });

      }
    }.start();

  }

  public void stopRadarAnimation() { 
    showToast("Animation stopped.");
    setRadarStep(0);
    refresh();
    update_status_line();
  }

  public boolean radarAnimationRunning() { 
      return radar_step < 0;
  }


   public long numberOfTiles() {
     return numberOfTiles(0,maxZoomLevel);
   }


   public long numberOfTiles(int minLevel,int maxLevel)
   { double[] lon = new double[2];
     double[] lat = new double[2];
     windowCoords(lat,lon);

     long total = 0;
     for(int zoom = minLevel; zoom<=maxLevel; zoom++)
     { projection.setZoomLevel(zoom);
       long tile_x1 = projection.longitudeToTileX(lon[0]);
       long tile_x2 = projection.longitudeToTileX(lon[1]);
       long tile_y1 = projection.latitudeToTileY(lat[1]);
       long tile_y2 = projection.latitudeToTileY(lat[0]);
       total += (tile_x2-tile_x1+1)*(tile_y2-tile_y1+1);
      }
     projection.setZoomLevel(zoomLevel);
     return total;
   }

   public long numberOfExistingTiles() {
     return numberOfExistingTiles(0,maxZoomLevel);
   }

   public long numberOfExistingTiles(int minLevel, int maxLevel)
   { double[] lon = new double[2];
     double[] lat = new double[2];
     windowCoords(lat,lon);

     long existing = 0;
     for(int zoom = minLevel; zoom<=maxLevel; zoom++)
     { projection.setZoomLevel(zoom);
       long tile_x1 = projection.longitudeToTileX(lon[0]);
       long tile_x2 = projection.longitudeToTileX(lon[1]);
       long tile_y1 = projection.latitudeToTileY(lat[1]);
       long tile_y2 = projection.latitudeToTileY(lat[0]);

       File dir = new File(tilePath + format("/%d",zoom));

       if (!dir.exists()) continue;

       File[] list = dir.listFiles();

       for (File f: list)
       { String fn = f.getName();
         int p = fn.indexOf(".");
         int q = fn.indexOf(".png");
         if (p == -1 || q == -1) continue;

         int x = Integer.parseInt(fn.substring(0,p));
         int y = Integer.parseInt(fn.substring(p+1,q));
         if (x >= tile_x1 && x <= tile_x2 && 
             y >= tile_y1 && y <= tile_y2) existing++;
        }
     }

     projection.setZoomLevel(zoomLevel);
     return existing;
   }


   public void downloadTiles(int zoom, int num)
   { double[] lon = new double[2];
     double[] lat = new double[2];
     windowCoords(lat,lon);

     int count = 0;

     showProgress(zoom,num);

     projection.setZoomLevel(zoom);

     long tile_x1 = projection.longitudeToTileX(lon[0]);
     long tile_x2 = projection.longitudeToTileX(lon[1]);
     long tile_y1 = projection.latitudeToTileY(lat[1]);
     long tile_y2 = projection.latitudeToTileY(lat[0]);

     if (tile_x1 < 0) tile_x1 = 0;
     if (tile_x2 >= projection.tileSize()) tile_x2 = projection.tileSize()-1;
     if (tile_y1 < 0) tile_y1 = 0;
     if (tile_y2 >= projection.tileSize()) tile_y2 = projection.tileSize()-1;

     boolean stopped = false;

     for(long x = tile_x1; x <= tile_x2; x++)
     { for(long y = tile_y1; y <= tile_y2; y++)
       { String png_name = format("%d/%d.%d.png",zoom,x,y);

         String url = format(tileURL,zoom,x,y);

        if (tileSource.startsWith("GMap")) {
          url += "?&key=" + gmap_api_key;
          if (tileSource == "GMap Road")
            url += "&session=" + gmap_session_token[0];
          if (tileSource == "GMap Terrain")
            url += "&session=" + gmap_session_token[1];
          if (tileSource == "GMap Satellite")
            url += "&session=" + gmap_session_token[2];

          if (url.endsWith("=")) url = null; // no session token
        }
        else
        if (tileSource.startsWith("HERE")) {
          url += "&apiKey=" + here_api_key;
        }


         File tile_file = new File(tilePath + "/" + png_name);

         if (tile_file.exists()) continue;

         tile_file.getParentFile().mkdirs();

         HttpClient http = new HttpClient(url,10000);
         if (http.getFile(tile_file)) count++;

         if (!showProgress(-1,count)) {
           setText1("stopped",0xff000000);
           stopped = true;
           break;
         }
       }
       if (stopped) break;
     }

     projection.setZoomLevel(zoomLevel);

   }


// map interface (IMapView)

    public void setRotationAnim(float phi) {
      animateTo(centerLat, centerLon, zoomLevel, phi);
    }


    public void setRotation(float phi) { 
        if (anim_step > 0) return;
        rotation = phi; 
        rotationMatrix.setRotate(rotation,mapWidth/2,mapHeight/2);
        postInvalidate();
    }

    public float getRotation() { return rotation; }

    public void  setHeading(float h) { 
      if (anim_step == 0) heading = h; 
    }

    public float getHeading() { return heading; }

    public void setMaxZoomLevel(int zoom) { maxZoomLevel = zoom; }
    public int  getMaxZoomLevel()         { return maxZoomLevel; }

    public boolean getShowRadar() { return showRadar; }

    public void setShowRadar(boolean b) 
    { 
      if (b == showRadar) return;

      if (b == false && radar_step != 0) {
        showToast("Animation canceled.");
      }

      if (b)
      { num_tiles = 2*tile_cols*tile_rows;
        setRadarStep(0);
        setZoomLevel(6);
       }
      else
        num_tiles = tile_cols * tile_rows;

      showRadar = b;
      refresh();
    }


    public void setRadarStep(int step) 
    { long current_time = System.currentTimeMillis();
      long sec = current_time/1000;
      sec = 600*((sec-60)/600);
      radar_sec = sec + step * 600;
      Date date = new Date(1000*radar_sec);
      radar_time = new SimpleDateFormat("HH:mm").format(date);
      radar_step = step;
    } 

    public void setTileProxyHost(String host, int port) {
       tileProxyHost = host;
       tileProxyPort = port;
    }

    public void setHereApiKey(String key) { here_api_key = key; }

    public void setGMapApiKey(String key) { gmap_api_key = key; }

    public void setGMapSessionToken(int i, String token) { 
      gmap_session_token[i] = token;
    }

    public String getTileSource() { return tileSource; }
    public String getTileURL()  { return tileURL; }
    public int    getTileSize() { return tileSize; }
    public int    getTileCols() { return tile_cols; }
    public int    getTileRows() { return tile_rows; }


    public String getStatistics() { 
      String txt = "tiles: " + num_tiles + "  threads: " + thread_max_count;
      if (mapTileStack != null) {
        txt += "  buffer: " + mapTileStack.getStatistics();
      }
      return txt;
    }


    void delete_folder(File file)
    { 
      File[] lst = file.listFiles();
      if (lst != null) {
        for (File f: lst) delete_folder(f);
      }
      file.delete();

/*
      // seems to block subsequent radar tile downloads
      try {
      Runtime.getRuntime().exec("rm -r -f " + file.getPath());
      } catch (Exception ex) { writeLog("MAP CLEANUP: " + ex.toString()); }
*/
    }


    void cleanMapFolder(File folder)
    {
      File[] lst = folder.listFiles();

      if (lst == null) return;

      for (File f: lst) 
      { if (f.isDirectory()) 
           cleanMapFolder(f);
        else
        { String name = f.getName();
          if (name.endsWith(".thr") || name.endsWith(".tag"))
          { f.delete();
            cleanup_files++;
           }
         }
       }
   }


   void cleanup(File folder) {

      writeLog("map cleanup: " + folder.getPath());

      cleanup_files = 0;
      cleanup_folders = 0;

      cleanMapFolder(folder);

      long current_sec = System.currentTimeMillis()/1000;
 
      File[] files = new File(folder,"Radar").listFiles();

      if (files != null) {
        for (File f: files) 
        { if (!f.isDirectory()) continue;
          String name = f.getName();
          long sec = Integer.parseInt(name);
          if (sec < current_sec - radarAnimationFrames*600) 
          { writeLog("delete folder: " + name);
            delete_folder(f);
            cleanup_folders++;
           }
        }
      }

      if (cleanup_files > 0) {
        writeLog("Map Cleanup: " + cleanup_files + " files");
      }

      if (cleanup_folders > 0) {
        writeLog("Radar Cleanup: " + cleanup_folders + " folders");
      }

    }


    int DipToPixels(float dp)
    { float dpi = context.getResources().getDisplayMetrics().densityDpi;
      return (int)(0.5 + dp * (dpi/160f));
     }


    void init(Context ctxt, String tile_src, File maps_dir) 
    {
      writeLog("MyMapView: init");

      context = ctxt;

      if (tile_src == null) tile_src = tileSourceNames[0];
      if (maps_dir == null) maps_dir = context.getFilesDir();

      userAgent = null;
      tile_errors = 0;

      maps_folder = maps_dir;

      writeLog("MapView: create maps_folder");
      if (!maps_folder.exists()) maps_folder.mkdir();

      File nomedia = new File(maps_folder,".nomedia");
      if (!nomedia.exists()) 
        try { nomedia.createNewFile(); } catch(IOException e) {}

      writeLog("MapView: clean up");

       new MyThread() {
         public void run() { cleanup(maps_folder); }
       }.start();


      zoomLevel = 10;
      centerLat = 49.555;
      centerLon =  6.823;

      projection = new MercatorProjection(tileSize);

      projection.setZoomLevel(zoomLevel);


      useDataConnection = true;
      //useDataConnection = false;

      setBackgroundColor(MAP_BG_COLOR);


      if (tile_src != null) setTileSource(tile_src);

      paint = new Paint();
      paint.setAntiAlias(true);
      paint.setFilterBitmap(true);
      paint.setDither(true);
      paint.setTextSize(DipToPixels(20));
      paint.setTextAlign(Align.CENTER);
      paint.setStrokeWidth(1);
  }


/*
    public MyMapView(Context ctxt) {
      super(ctxt);
      init(ctxt,null,null);
    }

    public MyMapView(Context ctxt,AttributeSet a) {
      super(ctxt,a);
      init(ctxt,null,null);
    }

    public MyMapView(Context ctxt,AttributeSet a, int i) {
      super(ctxt,a,i);
      init(ctxt,null,null);
    }

    public MyMapView(Context ctxt, String tile_src) {
        super(ctxt);
        init(ctxt,tile_src,null);
    }
*/

    public MyMapView(Context ctxt, String tile_src, File maps_dir) {
        super(ctxt);
        init(ctxt,tile_src,maps_dir);
    }


    public View getView() { return this; }

    public void setUseDataConnection(boolean b) { useDataConnection = b; }

    public void setDeveloperMode(boolean b) { developerMode = b; }

    public void setUserAgent(String user_agent)  { 
         writeLog("MapView: user_agent = " + user_agent);
         userAgent = user_agent; 
/*
         // check UserAgent
         new MyThread() {
            public void run() { 
              File file = new File(maps_folder,"user_agent.txt");
              String url="http://www.algobase.com/sTracks/user_agent.cgi";
              HttpClient http = new HttpClient(url,5000);
              if (userAgent != null) http.setUserAgent(userAgent);
              if (!http.getFile(file)) showToast(http.getError());
           }
        }.start();
*/
   }


    public void setTileSource(String src)
    { 
      int tileSourceIndex = -1;
      for(int i = 0; i < tileSourceNames.length; i++) { 
        if (src.equals(tileSourceNames[i])) tileSourceIndex = i;
      }

      if (tileSourceIndex == -1) tileSourceIndex = 0;

      tileSource = tileSourceNames[tileSourceIndex];
      tileURL = tileSourceUrls[tileSourceIndex];
      maxZoomLevel = tileSourceMaxZoom[tileSourceIndex];


/*
      if (tileSource.startsWith("OSM")) {
        setText2("\u00a9 OSM Contributors",0xff303030);
      }
      else
        if (tileSource.startsWith("GMap"))
          setText2("\u00a9 Google",0xff303030);
        else
          setText2("",0xff303030);
*/
        

      tilePath = maps_folder.getPath() + "/" + tileSource;

      if (mapTileStack != null) mapTileStack.clear();

      thread_max_count = 0;

      // force tile redraw
      buffer_tile_x = 0;
      buffer_tile_y = 0;

      setZoom(zoomLevel);
    }


    public void setMapDirectory(File dir)
    { maps_folder = dir;
      showToast(maps_folder.getPath());
      cleanMapFolder(maps_folder);
      setTileSource(tileSource);
    }


    public void setOverlay(IMapOverlay overlay)
    { mapOverlay = overlay;
      if (overlay != null) overlay.setMapView(this);
      postInvalidate();
    }

    public void getCenter(double coord[])
    { double center_x = projection.longitudeToPixelX(centerLon);
      double center_y = projection.latitudeToPixelY(centerLat);
      coord[0] = projection.pixelYToLatitude(center_y-drag_off_y);
      coord[1] = projection.pixelXToLongitude(center_x-drag_off_x);
    }

    public Location getCenter()
    { double[] coord = new double[2];
      getCenter(coord);
      Location loc = new Location("gps");
      loc.setLatitude(coord[0]);
      loc.setLongitude(coord[1]);
      return loc;
    }

    public void setZoomFactor(float f) { zoom_f = f; } 

    public void animToZoomLevel(final int z) { 

         new MyThread() {
           public void run() { 

            double current_z = zoomLevel + Math.log(zoom_f)/Math.log(2);

       
            if (z > zoomLevel + Math.log(zoom_f)/Math.log(2))
            { zoom_f = (int)(0.5f + zoom_f/0.025f) * 0.025f; 
              while (z > zoomLevel + Math.log(zoom_f)/Math.log(2) + 0.005)
              { zoom_f += 0.025f;
                postInvalidate();
                sleep(10);
               }
            }
            else
            if (z < zoomLevel + Math.log(zoom_f)/Math.log(2))
            { zoom_f = (int)(0.5f + zoom_f/0.0125f) * 0.0125f; 
              while (z < zoomLevel + Math.log(zoom_f)/Math.log(2) - 0.005)
              { zoom_f -= 0.0125f;
                postInvalidate();
                sleep(10);
               }
             }

             sleep(200);
             zoom_f = 1;
             anim_reset();
             setZoom(z);
           }
        }.start();
    }


    public void setZoomLevel(int z) { 
      if (z < 0) z = 0;
      if (z > maxZoomLevel) z = maxZoomLevel;
      if (z == zoomLevel) return;

      if (mapTileStack != null) mapTileStack.clear();
      if (radarTileStack != null) radarTileStack.clear();

      zoomLevel = z;
      projection.setZoomLevel(z);
    }


    public void setZoom(int z) { 
      setZoomLevel(z);
      postInvalidate();
    }

    public void setZoom0(int z) { setZoom(z); } 


    public void zoomIn() { 
      anim_reset();
      animateTo(centerLat,centerLon,zoomLevel+1,rotation);
    }

    public void zoomOut() { 
      anim_reset();
      animateTo(centerLat,centerLon,zoomLevel-1,rotation);
    }

    public int getMapWidth() { return mapWidth; }
    public int getMapHeight() { return mapHeight; }
    public int getZoomLevel() { return zoomLevel; }

    public int getBufferWidth() { return bufWidth; }
    public int getBufferHeight() { return bufHeight; }


    public int getZoomToFit(double lat_min, double lon_min,
                            double lat_max, double lon_max)
    {
      // empty area: (0,0,0,0)
      if (Math.abs(lon_min) < 0.001 && Math.abs(lon_max) < 0.001 &&
          Math.abs(lat_min) < 0.001 && Math.abs(lat_max) < 0.001)
        return 2;
    
/*
      double max_width = 0.9*viewWidth;
      double max_height = 0.9*viewHeight;
*/
      double max_width = 0.9*mapWidth;
      double max_height = 0.9*mapHeight;

      if (max_width == 0 || max_height == 0)
      { WindowManager wm = 
           ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)); 
        Display display = wm.getDefaultDisplay(); 
        Point pt = new Point();
        display.getSize(pt);
        max_width = 0.9*pt.x;
        max_height = 0.7*pt.y;
       }

      //showToast(format("max_w = %f max_h = %f",max_width,max_height));

      int z = maxZoomLevel;
    
      while (z > 0)
      { projection.setZoomLevel(z);
        double xpix0 = projection.longitudeToPixelX(lon_min);
        double xpix1 = projection.longitudeToPixelX(lon_max);
        double ypix0 = projection.longitudeToPixelX(lat_min);
        double ypix1 = projection.longitudeToPixelX(lat_max);
        if (xpix1-xpix0  < max_width && ypix1-ypix0  < max_height) break;
        z--;
      }
    
     projection.setZoomLevel(zoomLevel);

     return z;
    }


    public void zoomToFit(double lat_min, double lon_min,
                          double lat_max, double lon_max)
    {
      double lon = (lon_min + lon_max)/2;
      double lat = (lat_min + lat_max)/2;

      //double dmax = 0.0025;
      double dmax = 0.0005;

      if (lat_min > lat-dmax) lat_min = lat-dmax;
      if (lat_max < lat+dmax) lat_max = lat+dmax;
      if (lon_min > lon-dmax) lon_min = lon-dmax;
      if (lon_max < lon+dmax) lon_max = lon+dmax;

      int z = getZoomToFit(lat_min,lon_min,lat_max,lon_max);

      //animateTo(lat,lon,z,0);

      setAnimFinish( new Runnable() {
          public void run() { setRotationAnim(0); }
      }, 100);

      animateTo(lat,lon,z);

    }


    public void zoomToSpan(double lat_span, double lon_span) 
    { 
      int z = maxZoomLevel;
      int width = mapWidth;
      int height = mapHeight;

      while(z > 0 && (projection.lonSpanToPixels(lon_span,z) > width || 
                      projection.latSpanToPixels(lat_span,z) > height)) z--;
      setZoom(z);
     }

    public void setCenterSilent(double lat, double lon)
    { if (lon < -180 || lon > +180 || lat < -85 || lat > +85) return;
      centerLat = lat;
      centerLon = lon;
    }

    public void setCenterSilent(Location loc)
    { if (loc != null) setCenterSilent(loc.getLatitude(),loc.getLongitude()); }


    public void setCenter(double lat, double lon)
    { if (lon < -180 || lon > +180 || lat < -85 || lat > +85) return;
      centerLat = lat;
      centerLon = lon;
      postInvalidate();
     }

    public void setCenter(Location loc)
    { if (loc != null) setCenter(loc.getLatitude(), loc.getLongitude()); }

    private void shiftCenter(float dx, float dy)
    { double xpix = projection.longitudeToPixelX(centerLon) + dx;
      double ypix = projection.latitudeToPixelY(centerLat) + dy;
      centerLon = projection.pixelXToLongitude(xpix);
      centerLat = projection.pixelYToLatitude(ypix);
    }


    public void animateTo(final double lat, final double lon, final int z, 
                                                              final float phi)
    { 
      if (mapWidth <= 0 || mapHeight <= 0) {
         // layout not initialized 
         return;
      }

      // no animation when dragging or zooming or ongoing animation

      if (drag_off_x != 0) {
        //showToast("drag_off_x = " + drag_off_x);
        //drag_off_x = 0;
        return;
      }

      if (drag_off_y != 0) {
        //showToast("drag_off_y = " + drag_off_y);
        //drag_off_y = 0;
        return;
      }

      if (zoom_f != 1.0f || anim_zoom_f != 1.0f) return;

      if (keep_moving) return;

      if (anim_step > 0) return;


      int zoom_d = 0;

      if (z != -1) zoom_d = z - zoomLevel;

      buffer_tile_x = 0;
      buffer_tile_y = 0;

      // we can animate a change of one zoom level only

      if (zoom_d > 1)
      { setZoomLevel(zoomLevel + zoom_d - 1);
        zoom_d = +1;
       }

      if (zoom_d < -1)
      { setZoomLevel(zoomLevel + zoom_d + 1);
        zoom_d = -1;
       }


      double lat0 = centerLat;
      double lon0 = centerLon;

      anim_lat_delta = lat - lat0;
      anim_lon_delta = lon - lon0;

/*
if (Math.abs(anim_lat_delta) < 0.00005 && 
    Math.abs(anim_lon_delta) < 0.00005 && zoom_d == 0) return;
*/


      double pix_dx = projection.longitudeToPixelX(lon) -
                      projection.longitudeToPixelX(lon0);

      double pix_dy = projection.latitudeToPixelY(lat) -
                      projection.latitudeToPixelY(lat0);

      double pix_dist = Math.hypot(pix_dx,pix_dy);

      anim_xpix_delta = (float)pix_dx;
      anim_ypix_delta = (float)pix_dy;


      int max_d = 2000;

      if (pix_dist > (max_d+1))
      { double f = (pix_dist-max_d)/pix_dist;
        lat0 += f*anim_lat_delta;
        lon0 += f*anim_lon_delta;
        centerLat = lat0;
        centerLon = lon0;
/*
        anim_lat_delta *= (1-f);
        anim_lon_delta *= (1-f);
        anim_xpix_delta *= (1-f);
        anim_ypix_delta *= (1-f);
        pix_dist = max_d;
*/
        postInvalidate();
        //animateTo(lat,lon,z,phi);

        handler.post(new Runnable() {
           public void run() { animateTo(lat,lon,z,phi); }
        });

        return;
      }

      float phi_delta = phi - rotation;
      if (phi_delta > +180) phi_delta -= 360;
      if (phi_delta < -180) phi_delta += 360;


      if (zoom_d == 0 && pix_dist < 3 && Math.abs(phi_delta) < 1)
      { // nothing to do
        postInvalidate(); // calls animFinish (if defined)
        return;
       }

      anim_step_num = (int)(pix_dist/25);


      int rotate_steps = (int)(0.5f + Math.abs(phi_delta)/3);
      if (rotate_steps > anim_step_num) anim_step_num = rotate_steps;

      if (zoom_d != 0) { 
       if (anim_step_num < 20) anim_step_num = 20;
      }

      if (anim_step_num > 50) anim_step_num = 50;
      if (anim_step_num < 10) anim_step_num = 10;

      //if (anim_step_num == 0) anim_step_num = 1;

//showToast("steps = " + anim_step_num);

      anim_step = 0;

      anim_rotate_delta = phi_delta/anim_step_num;
      anim_zoom_f = 1.0f;
      anim_zoom_delta = 0.0f;
      anim_zoom_target = zoomLevel;

      if (zoom_d > 0) 
      { anim_zoom_delta = 1.0f/anim_step_num;
        anim_zoom_target++;
       }
      else
      if (zoom_d < 0) 
      { anim_zoom_delta = -0.5f/anim_step_num;
        anim_zoom_target--;
       }


      anim_lat_delta /= anim_step_num;
      anim_lon_delta /= anim_step_num;

      anim_xpix_delta /= anim_step_num;
      anim_ypix_delta /= anim_step_num;

      postInvalidate();
    }


    public void animateTo(double lat, double lon, int z)
    { animateTo(lat,lon,z,rotation); }

    public void animateTo(double lat, double lon)
    { animateTo(lat,lon,-1); }

    public void animateTo()
    { animateTo(centerLat,centerLon,zoomLevel,rotation); }

    public void animateZoom(int z)
    { animateTo(centerLat,centerLon,z,rotation); }


    public void animateTo(Location loc, int z, float heading)
    { animateTo(loc.getLatitude(), loc.getLongitude(),z,heading); }

    public void animateTo(Location loc, int z)
    { animateTo(loc.getLatitude(), loc.getLongitude(),z); }

    public void animateTo(Location loc)
    { animateTo(loc.getLatitude(), loc.getLongitude()); }



    public boolean isSatellite() { 
      return tileSource.endsWith("Satellite");
    }


   public boolean isStreetView() { return false; }
   public void    setStreetView(boolean b) {} 

   public boolean isTraffic() { return false; }
   public void    setTraffic(boolean b) {} 


   public void windowCoords(double[] lat, double[] lon)
   { 
     double center_x = projection.longitudeToPixelX(centerLon);
     double center_y = projection.latitudeToPixelY(centerLat);

     lon[0] = projection.pixelXToLongitude(center_x - mapWidth/2);
     lon[1] = projection.pixelXToLongitude(center_x + mapWidth/2);
     lat[0] = projection.pixelYToLatitude(center_y + mapHeight/2);
     lat[1] = projection.pixelYToLatitude(center_y - mapHeight/2);
   }


   public void toPixels(double lat, double lon, Point p) 
   { 
     double center_x = projection.longitudeToPixelX(centerLon);
     double center_y = projection.latitudeToPixelY(centerLat);

/*
     double px = projection.longitudeToPixelX(lon) - (center_x - mapWidth/2);
     double py = projection.latitudeToPixelY(lat) - (center_y - mapHeight/2);
*/
     double px = projection.longitudeToPixelX(lon) - (center_x - bufWidth/2);
     double py = projection.latitudeToPixelY(lat) - (center_y - bufHeight/2);

     p.x = (int)(0.5 + px);
     p.y = (int)(0.5 + py);
   }


   public void toPixels(Location loc, Point p)
   { if (loc != null) toPixels(loc.getLatitude(), loc.getLongitude(),p); }


   public Location fromPixels(int x, int y)
   { 
     double center_x = projection.longitudeToPixelX(centerLon);
     double center_y = projection.latitudeToPixelY(centerLat);

     double lon = projection.pixelXToLongitude(center_x - mapWidth/2 + x);
     double lat = projection.pixelYToLatitude(center_y - mapHeight/2 + y);

     Location loc  = new Location("map");
     loc.setLatitude(lat);
     loc.setLongitude(lon);
     return loc;
   }


   public float metersToEquatorPixels(float meter) {
    return projection.meterToPixels(meter);
   }

   public void refresh() { 
      // force tile buffer redraw
      buffer_tile_x = 0;
      buffer_tile_y = 0;
      postInvalidate(); 
   }

/*
   public void invalidate() { super.invalidate(); }
   public void postInvalidate() { super.postInvalidate(); }
*/

   public void setAnimFinish(Runnable r, long delay) { 
           animFinish = r; 
           animFinishDelay = delay;
   }
}

