package btools.routingapp; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import android.app.Activity; import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.os.AsyncTask; import android.os.PowerManager; import android.os.StatFs; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.widget.Toast; import btools.mapaccess.PhysicalFile; public class BInstallerView extends View { private static final int MASK_SELECTED_RD5 = 1; private static final int MASK_SELECTED_CD5 = 2; private static final int MASK_INSTALLED_RD5 = 4; private static final int MASK_INSTALLED_CD5 = 8; private static final int[] maskTransitions = new int[] { 3, 2, 0, 1, 6, 6, 4, 4, 9, 8, 9, 8, 12, 12, 12, 12 }; private int imgw; private int imgh; private float lastDownX; private float lastDownY; private Bitmap bmp; private float viewscale; private float[] testVector = new float[2]; private int[] tileStatus; private boolean tilesVisible = false; private long lastDownTime = 0; private long availableSize; private String baseDir; private boolean isDownloading = false; private transient boolean downloadCanceled = false; private long currentDownloadSize; private String currentDownloadFile = ""; private String downloadAction = ""; private long totalSize = 0; private long rd5Tiles = 0; private long cd5Tiles = 0; protected String baseNameForTile( int tileIndex ) { int lon = (tileIndex % 72 ) * 5 - 180; int lat = (tileIndex / 72 ) * 5 - 90; String slon = lon < 0 ? "W" + (-lon) : "E" + lon; String slat = lat < 0 ? "S" + (-lat) : "N" + lat; return slon + "_" + slat; } private int gridPos2Tileindex( int ix, int iy ) { return (35-iy)*72 + ( ix >= 70 ? ix-70: ix+2 ); } private int tileForBaseName( String basename ) { String uname = basename.toUpperCase(); int idx = uname.indexOf( "_" ); if ( idx < 0 ) return -1; String slon = uname.substring( 0, idx ); String slat = uname.substring( idx+1 ); int ilon = slon.charAt(0) == 'W' ? -Integer.valueOf( slon.substring(1) ) : ( slon.charAt(0) == 'E' ? Integer.valueOf( slon.substring(1) ) : -1 ); int ilat = slat.charAt(0) == 'S' ? -Integer.valueOf( slat.substring(1) ) : ( slat.charAt(0) == 'N' ? Integer.valueOf( slat.substring(1) ) : -1 ); if ( ilon < -180 || ilon >= 180 || ilon % 5 != 0 ) return -1; if ( ilat < - 90 || ilat >= 90 || ilat % 5 != 0 ) return -1; return (ilon+180) / 5 + 72*((ilat+90)/5); } public boolean isDownloadCanceled() { return downloadCanceled; } private void toggleDownload() { if ( isDownloading ) { downloadCanceled = true; downloadAction = "Canceling..."; return; } int tidx_min = -1; boolean isCd5_min = false; int min_size = Integer.MAX_VALUE; // prepare download list for( int ix=0; ix<72; ix++ ) { for( int iy=0; iy<36; iy++ ) { int tidx = gridPos2Tileindex( ix, iy ); for( int mask = 1; mask <= 2; mask *= 2 ) { int s = tileStatus[tidx]; boolean isCd5 = (mask == 2); if ( ( s & mask ) != 0 && ( s & (mask*4)) == 0 ) { int tilesize = isCd5 ? BInstallerSizes.cd5_sizes[tidx] : BInstallerSizes.rd5_sizes[tidx]; if ( tilesize > 0 && tilesize < min_size ) { tidx_min = tidx; isCd5_min = isCd5; min_size = tilesize; } } } } } if ( tidx_min != -1 ) { startDownload( tidx_min, isCd5_min ); } } private void startDownload( int tileIndex, boolean isCd5 ) { String namebase = baseNameForTile( tileIndex ); String baseurl = "http://brouter.de/brouter/segments3/"; currentDownloadFile = namebase + (isCd5 ? ".cd5" : ".rd5" ); String url = baseurl + (isCd5 ? "carsubset/" : "" ) + currentDownloadFile; isDownloading = true; downloadCanceled = false; currentDownloadSize = 0; downloadAction = "Connecting... "; final DownloadTask downloadTask = new DownloadTask(getContext()); downloadTask.execute( url ); } public void downloadDone( boolean success ) { isDownloading = false; if ( success ) { scanExistingFiles(); toggleDownload(); // keep on if no error } invalidate(); } private int tileIndex( float x, float y ) { int ix = (int)(72.f * x / bmp.getWidth()); int iy = (int)(36.f * y / bmp.getHeight()); if ( ix >= 0 && ix < 72 && iy >= 0 && iy < 36 ) return gridPos2Tileindex(ix, iy); return -1; } private void clearTileSelection( int mask ) { // clear selection if zooming out for( int ix=0; ix<72; ix++ ) for( int iy=0; iy<36; iy++ ) { int tidx = gridPos2Tileindex( ix, iy ); tileStatus[tidx] ^= tileStatus[tidx] & mask; } } // get back the current image scale private float currentScale() { testVector[1] = 1.f; mat.mapVectors(testVector); return testVector[1] / viewscale; } private void scanExistingFiles() { clearTileSelection( MASK_INSTALLED_CD5 | MASK_INSTALLED_RD5 ); scanExistingFiles( new File( baseDir + "/brouter/segments2" ), ".rd5", MASK_INSTALLED_RD5 ); scanExistingFiles( new File( baseDir + "/brouter/segments2/carsubset" ), ".cd5", MASK_INSTALLED_CD5 ); StatFs stat = new StatFs(baseDir); availableSize = (long)stat.getAvailableBlocks()*stat.getBlockSize(); } private void scanExistingFiles( File dir, String suffix, int maskBit ) { String[] fileNames = dir.list(); if ( fileNames == null ) return; for( String fileName : fileNames ) { if ( fileName.endsWith( suffix ) ) { String basename = fileName.substring( 0, fileName.length() - suffix.length() ); int tidx = tileForBaseName( basename ); tileStatus[tidx] |= maskBit; tileStatus[tidx] ^= tileStatus[tidx] & (maskBit >> 2); } } } private Matrix mat; public void startInstaller() { baseDir = ConfigHelper.getBaseDir( getContext() ); try { AssetManager assetManager = getContext().getAssets(); InputStream istr = assetManager.open("world.png"); bmp = BitmapFactory.decodeStream(istr); istr.close(); } catch( IOException io ) { throw new RuntimeException( "cannot read world.png from assets" ); } tileStatus = new int[72*36]; scanExistingFiles(); float scaleX = imgw / ((float)bmp.getWidth()); float scaley = imgh / ((float)bmp.getHeight()); viewscale = scaleX < scaley ? scaleX : scaley; mat = new Matrix(); mat.postScale( viewscale, viewscale ); tilesVisible = false; } public void stopInstaller() { downloadCanceled = true; bmp = null; tileStatus = null; } public BInstallerView(Context context) { super(context); DisplayMetrics metrics = new DisplayMetrics(); ((Activity)getContext()).getWindowManager().getDefaultDisplay().getMetrics(metrics); imgw = metrics.widthPixels; imgh = metrics.heightPixels; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { } private void toast( String msg ) { Toast.makeText(getContext(), msg, Toast.LENGTH_LONG ).show(); } @Override protected void onDraw(Canvas canvas) { if ( !isDownloading ) { canvas.setMatrix( mat ); canvas.drawBitmap(bmp, 0,0, null); } // draw 5*5 lattice starting at scale=3 int iw = bmp.getWidth(); int ih = bmp.getHeight(); float fw = iw / 72.f; float fh = ih / 36.f; boolean drawGrid = tilesVisible && !isDownloading; if ( drawGrid ) { Paint pnt_1 = new Paint(); pnt_1.setColor(Color.GREEN); for( int ix=1; ix<72; ix++ ) { float fx = fw*ix; canvas.drawLine( fx, 0, fx, ih, pnt_1); } for( int iy=1; iy<36; iy++ ) { float fy = fh*iy; canvas.drawLine( 0, fy, iw, fy, pnt_1); } } rd5Tiles = 0; cd5Tiles = 0; totalSize = 0; Paint pnt_2 = new Paint(); pnt_2.setColor(Color.GRAY); pnt_2.setStrokeWidth(1); drawSelectedTiles( canvas, pnt_2, fw, fh, MASK_INSTALLED_RD5, MASK_INSTALLED_CD5, false, drawGrid ); pnt_2.setColor(Color.GREEN); pnt_2.setStrokeWidth(2); drawSelectedTiles( canvas, pnt_2, fw, fh, MASK_SELECTED_RD5, MASK_SELECTED_CD5, true, drawGrid ); canvas.setMatrix( null ); Paint paint = new Paint(); paint.setColor(Color.RED); long mb = 1024*1024; if ( isDownloading ) { String sizeHint = currentDownloadSize > 0 ? " (" + ((currentDownloadSize + mb-1)/mb) + " MB)" : ""; paint.setTextSize(30); canvas.drawText( "Loading " + currentDownloadFile + sizeHint, 30, (imgh/3)*2-30, paint); canvas.drawText( downloadAction, 30, (imgh/3)*2, paint); } if ( !tilesVisible ) { paint.setTextSize(40); canvas.drawText( "Zoom in to see grid!", 30, (imgh/3)*2, paint); } paint.setTextSize(20); String totmb = ((totalSize + mb-1)/mb) + " MB"; String freemb = ((availableSize + mb-1)/mb) + " MB"; canvas.drawText( "Selected: full=" + rd5Tiles + " carsubset=" + cd5Tiles, 10, 25, paint ); canvas.drawText( "Size=" + totmb + " Free=" + freemb , 10, 45, paint ); String btnText = null; if ( isDownloading ) btnText = "Cancel Download"; else if ( rd5Tiles + cd5Tiles > 0 ) btnText = "Start Download"; if ( btnText != null ) { canvas.drawLine( imgw-btnw, imgh-btnh, imgw-btnw, imgh-2, paint); canvas.drawLine( imgw-btnw, imgh-btnh, imgw-2, imgh-btnh, paint); canvas.drawLine( imgw-btnw, imgh-btnh, imgw-btnw, imgh-2, paint); canvas.drawLine( imgw-2, imgh-btnh, imgw-2, imgh-2, paint); canvas.drawLine( imgw-btnw, imgh-2, imgw-2, imgh-2, paint); canvas.drawText( btnText , imgw-btnw+5, imgh-10, paint ); } } int btnh = 40; int btnw = 160; float tx, ty; private void drawSelectedTiles( Canvas canvas, Paint pnt, float fw, float fh, int maskRd5, int maskCd5, boolean doCount, boolean doDraw ) { for( int ix=0; ix<72; ix++ ) for( int iy=0; iy<36; iy++ ) { int tidx = gridPos2Tileindex( ix, iy ); boolean isRd5 = (tileStatus[tidx] & maskRd5) != 0; boolean isCd5 = (tileStatus[tidx] & maskCd5) != 0; if ( isRd5 || isCd5 ) { int tilesize = BInstallerSizes.rd5_sizes[tidx]; if ( tilesize > 0 ) { if ( doCount ) { if ( isRd5) { rd5Tiles++; totalSize += BInstallerSizes.rd5_sizes[tidx]; }; if ( isCd5) { cd5Tiles++; totalSize += BInstallerSizes.cd5_sizes[tidx]; }; } if ( !doDraw ) continue; if ( isRd5 ) canvas.drawLine( fw*ix, fh*iy, fw*(ix+1), fh*(iy+1), pnt); if ( isCd5 ) canvas.drawLine( fw*ix, fh*(iy+1), fw*(ix+1), fh*iy, pnt); canvas.drawLine( fw*ix, fh*iy, fw*(ix+1), fh*iy, pnt); canvas.drawLine( fw*ix, fh*(iy+1), fw*(ix+1), fh*(iy+1), pnt); canvas.drawLine( fw*ix, fh*iy, fw*ix, fh*(iy+1), pnt); canvas.drawLine( fw*(ix+1), fh*iy, fw*(ix+1), fh*(iy+1), pnt); } } } } @Override public boolean onTouchEvent(MotionEvent event) { // get pointer index from the event object int pointerIndex = event.getActionIndex(); // get pointer ID int pointerId = event.getPointerId(pointerIndex); // get masked (not specific to a pointer) action int maskedAction = event.getActionMasked(); switch (maskedAction) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: { lastDownX = event.getX(); lastDownY = event.getY(); break; } case MotionEvent.ACTION_MOVE: { // a pointer was moved if ( isDownloading ) break; int np = event.getPointerCount(); int nh = event.getHistorySize(); if ( nh == 0 ) break; float x0 = event.getX( 0 ); float y0 = event.getY( 0 ); float hx0 = event.getHistoricalX(0,0); float hy0 = event.getHistoricalY(0,0); if ( np > 1 ) // multi-touch { float x1 = event.getX( 1 ); float y1 = event.getY( 1 ); float hx1 = event.getHistoricalX(1,0); float hy1 = event.getHistoricalY(1,0); float r = (float)Math.sqrt( (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) ); float hr = (float)Math.sqrt( (hx1-hx0)*(hx1-hx0) + (hy1-hy0)*(hy1-hy0) ); if ( hr > 10. ) { float ratio = r/hr; float mx = (x1+x0)/2.f; float my = (y1+y0)/2.f; float scale = currentScale(); float newscale = scale*ratio; if ( newscale > 10.f ) ratio *= (10.f / newscale ); if ( newscale < 0.5f ) ratio *= (0.5f / newscale ); mat.postScale( ratio, ratio, mx, my ); mat.postScale( ratio, ratio, mx, my ); boolean tilesv = currentScale() >= 3.f; if ( tilesVisible && !tilesv ) { clearTileSelection( MASK_SELECTED_CD5 | MASK_SELECTED_RD5 ); } tilesVisible = tilesv; } break; } mat.postTranslate( x0-hx0, y0-hy0 ); break; } case MotionEvent.ACTION_UP: lastDownTime = event.getEventTime() - event.getDownTime(); if ( lastDownTime < 5 || lastDownTime > 500 ) { break; } if ( Math.abs(lastDownX - event.getX() ) > 10 || Math.abs(lastDownY - event.getY() ) > 10 ) { break; } // download button? if ( rd5Tiles + cd5Tiles > 0 && event.getX() > imgh - btnh && event.getY() > imgh-btnh ) { toggleDownload(); invalidate(); break; } if ( isDownloading || !tilesVisible ) break; Matrix imat = new Matrix(); if ( mat.invert(imat) ) { float[] touchpoint = new float[2]; touchpoint[0] = event.getX(); touchpoint[1] = event.getY(); imat.mapPoints(touchpoint); int tidx = tileIndex( touchpoint[0], touchpoint[1] ); if ( tidx != -1 ) { tileStatus[tidx] = maskTransitions[tileStatus[tidx]]; } tx = touchpoint[0]; ty = touchpoint[1]; } break; case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: { // TODO use data break; } } invalidate(); return true; } // usually, subclasses of AsyncTask are declared inside the activity class. // that way, you can easily modify the UI thread from here private class DownloadTask extends AsyncTask { private Context context; private PowerManager.WakeLock mWakeLock; public DownloadTask(Context context) { this.context = context; } @Override protected String doInBackground(String... sUrls) { InputStream input = null; OutputStream output = null; HttpURLConnection connection = null; String surl = sUrls[0]; String fname = null; File tmp_file = null; try { try { URL url = new URL(surl); connection = (HttpURLConnection) url.openConnection(); connection.connect(); // expect HTTP 200 OK, so we don't mistakenly save error report // instead of the file if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { return "Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage(); } // this will be useful to display download percentage // might be -1: server did not report the length int fileLength = connection.getContentLength(); currentDownloadSize = fileLength; if ( fileLength > availableSize ) return "not enough space on sd-card"; // download the file input = connection.getInputStream(); int slidx = surl.lastIndexOf( "segments3/" ); fname = baseDir + "/brouter/segments2/" + surl.substring( slidx+10 ); tmp_file = new File( fname + "_tmp" ); if ( new File( fname ).exists() ) return "internal error: file exists: " + fname; output = new FileOutputStream( tmp_file ); byte data[] = new byte[4096]; long total = 0; long t0 = System.currentTimeMillis(); int count; while ((count = input.read(data)) != -1) { if (isDownloadCanceled()) { return "Download canceled!"; } total += count; // publishing the progress.... if (fileLength > 0) // only if total length is known publishProgress((int) (total * 100 / fileLength)); output.write(data, 0, count); // enforce < 200kB/s long dt = t0 + total/200 - System.currentTimeMillis(); if ( dt > 0 ) { try { Thread.sleep( dt ); } catch( InterruptedException ie ) {} } } } catch (Exception e) { return e.toString(); } finally { try { if (output != null) output.close(); if (input != null) input.close(); } catch (IOException ignored) { } if (connection != null) connection.disconnect(); } publishProgress( 101 ); String check_result = PhysicalFile.checkFileIntegrity( tmp_file ); if ( check_result != null ) return check_result; if ( !tmp_file.renameTo( new File( fname ) ) ) { return "Could not rename to " + fname; } return null; } finally { if ( tmp_file != null ) tmp_file.delete(); // just to be sure } } @Override protected void onPreExecute() { super.onPreExecute(); // take CPU lock to prevent CPU from going off if the user // presses the power button during download PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); mWakeLock.acquire(); } @Override protected void onProgressUpdate(Integer... progress) { String newAction = progress[0] == 101 ? "Verifying.." : "Progress " + progress[0] + "%"; if ( !newAction.equals( downloadAction ) ) { downloadAction = newAction; invalidate(); } } @Override protected void onPostExecute(String result) { mWakeLock.release(); downloadDone( result == null ); if (result != null) Toast.makeText(context,"Download error: "+result, Toast.LENGTH_LONG).show(); else Toast.makeText(context,"File downloaded", Toast.LENGTH_SHORT).show(); } } // download task }