content: package btools.routingapp;
  
  import android.Manifest;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.content.res.AssetManager;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Paint;
  import android.util.Log;
  import android.view.View;
  import android.widget.Toast;
  
  import androidx.core.app.ActivityCompat;
  import androidx.core.content.ContextCompat;
  import androidx.core.content.FileProvider;
  
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.FileReader;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipInputStream;
  
  import btools.expressions.BExpressionMetaData;
  import btools.mapaccess.OsmNode;
  import btools.router.OsmNodeNamed;
  import btools.router.OsmNogoPolygon;
  import btools.router.OsmNogoPolygon.Point;
  import btools.router.OsmTrack;
  import btools.router.RoutingContext;
  import btools.router.RoutingEngine;
  import btools.router.RoutingHelper;
  import btools.router.RoutingParamCollector;
  
  import btools.util.CheapRuler;
  
  public class BRouterView extends View {
  
    private static final String TAG = "BRouterView";
  
    private final int memoryClass;
    RoutingEngine cr;
    private int imgw;
    private int imgh;
    private int centerLon;
    private int centerLat;
    private double scaleLon;  // ilon -> pixel
    private double scaleLat;  // ilat -> pixel
    private double scaleMeter2Pixel;
    private List<OsmNodeNamed> wpList;
    private List<OsmNodeNamed> nogoList;
    private List<OsmNodeNamed> nogoVetoList;
    private OsmTrack rawTrack;
    private File retryBaseDir;
    private File modesDir;
    private File tracksDir;
    private File segmentDir;
    private File profileDir;
    private String profileName;
    private boolean waitingForSelection = false;
    private boolean waitingForMigration = false;
    private String rawTrackPath;
    private String oldMigrationPath;
    private String trackOutfile;
    private boolean needsViaSelection;
    private boolean needsNogoSelection;
    private boolean needsWaypointSelection;
    private long lastDataTime = System.currentTimeMillis();
    private CoordinateReader cor;
    private int[] imgPixels;
    private long lastTs = System.currentTimeMillis();
    private long startTime = 0L;
  
    public BRouterView(Context context, int memoryClass) {
      super(context);
      this.memoryClass = memoryClass;
    }
  
    public void stopRouting() {
      if (cr != null) cr.terminate();
    }
  
    public void init(boolean silent) {
      try {
        // get base dir from private file
        File baseDir = ConfigHelper.getBaseDir(getContext());
        // check if valid
        boolean bdValid = false;
        if (baseDir != null) {
          bdValid = baseDir.isDirectory();
          File brd = new File(baseDir, "brouter");
          if (brd.isDirectory()) {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q &&
              !brd.getAbsolutePath().contains("/Android/media/btools.routingapp")) {
  
              // don't ask twice
              String version = "v" + getContext().getString(R.string.app_version);
              File vFile = new File(brd, "profiles2/" + version);
              if (vFile.exists()) {
                startSetup(baseDir, false, silent);
                return;
              }
              String message = "(previous basedir " + baseDir + " has to migrate )";
  
              ((BRouterActivity) getContext()).selectBasedir(((BRouterActivity) getContext()).getStorageDirectories(), message);
              waitingForSelection = true;
              waitingForMigration = true;
              oldMigrationPath = brd.getAbsolutePath();
            } else {
              startSetup(baseDir, false, silent);
            }
            return;
          }
        }
        String message = baseDir == null ? "(no basedir configured previously)" : "(previous basedir " + baseDir
          + (bdValid ? " does not contain 'brouter' subfolder)" : " is not valid)");
  
        ((BRouterActivity) getContext()).selectBasedir(((BRouterActivity) getContext()).getStorageDirectories(), message);
        waitingForSelection = true;
      } catch (Exception e) {
        String msg = e instanceof IllegalArgumentException ? e.getMessage() : e.toString();
  
        AppLogger.log(msg);
        AppLogger.log(AppLogger.formatThrowable(e));
  
        ((BRouterActivity) getContext()).showErrorMessage(msg);
      }
    }
  
    public void startSetup(File baseDir, boolean storeBasedir, boolean silent) {
      if (baseDir == null) {
        baseDir = retryBaseDir;
        retryBaseDir = null;
      }
  
      if (storeBasedir) {
        File td = new File(baseDir, "brouter");
        try {
          td.mkdirs();
        } catch (Exception e) {
          Log.d(TAG, "Error creating base directory: " + e.getMessage());
          Log.e(TAG, Log.getStackTraceString(e));
        }
  
        if (!td.isDirectory()) {
          if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
            retryBaseDir = baseDir;
            ActivityCompat.requestPermissions((BRouterActivity) getContext(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
          } else {
            ((BRouterActivity) getContext()).selectBasedir(((BRouterActivity) getContext()).getStorageDirectories(), "Cannot access " + baseDir.getAbsolutePath() + "; select another");
          }
          return;
        }
        ConfigHelper.writeBaseDir(getContext(), baseDir);
      }
      try {
        cor = null;
  
        String basedir = baseDir.getAbsolutePath();
        AppLogger.log("using basedir: " + basedir);
  
        populateBasedir(basedir);
  
        // new init is done move old files
        if (waitingForMigration) {
          Log.d(TAG, "path " + oldMigrationPath + " " + basedir);
          Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
              if (!oldMigrationPath.equals(basedir + "/brouter"))
                moveFolders(oldMigrationPath, basedir + "/brouter");
            }});
          t.start();
          try {
            t.join(500);
          } catch (InterruptedException e) {
            Log.e(TAG, Log.getStackTraceString(e));
          }
          waitingForMigration = false;
        }
  
        cor = CoordinateReader.obtainValidReader(basedir);
  
        wpList = cor.waypoints;
        nogoList = cor.nogopoints;
        nogoVetoList = new ArrayList<>();
  
        needsViaSelection = wpList.size() > 2;
        needsNogoSelection = nogoList.size() > 0;
        needsWaypointSelection = wpList.size() == 0;
  
        if (cor.tracksdir != null) {
          tracksDir = new File(cor.basedir, cor.tracksdir);
          assertDirectoryExists("track directory", tracksDir, null, null);
        }
        if (tracksDir == null) {
          tracksDir = new File(basedir, "brouter"); // fallback
        }
  
        String[] fileNames = profileDir.list();
        ArrayList<String> profiles = new ArrayList<>();
  
        boolean lookupsFound = false;
        if (fileNames != null) {
          for (String fileName : fileNames) {
            if (fileName.endsWith(".brf")) {
              profiles.add(fileName.substring(0, fileName.length() - 4));
            }
            if (fileName.equals("lookups.dat"))
              lookupsFound = true;
          }
        }
  
        // add a "last timeout" dummy profile
        File lastTimeoutFile = new File(modesDir + "/timeoutdata.txt");
        long lastTimeoutTime = lastTimeoutFile.lastModified();
        if (lastTimeoutTime > 0 &&
          lastTimeoutFile.length() > 0 &&
          System.currentTimeMillis() - lastTimeoutTime < 1800000) {
          BufferedReader br = new BufferedReader(new FileReader(lastTimeoutFile));
          String repeatProfile = br.readLine();
          br.close();
          profiles.add(0, "<repeat:" + repeatProfile + ">");
        }
  
        if (!lookupsFound) {
          throw new IllegalArgumentException("The profile-directory " + profileDir + " does not contain the lookups.dat file."
            + " see brouter.de/brouter for setup instructions.");
        }
        if (profiles.size() == 0) {
          throw new IllegalArgumentException("The profile-directory " + profileDir + " contains no routing profiles (*.brf)."
            + " see brouter.de/brouter for setup instructions.");
        }
        if (silent) {
          Intent intent = new Intent(getContext(), BInstallerActivity.class);
          getContext().startActivity(intent);
          return;
        };
  
        if (!RoutingHelper.hasDirectoryAnyDatafiles(segmentDir)) {
          ((BRouterActivity) getContext()).selectProfile(profiles.toArray(new String[0]), false);
          ((BRouterActivity) getContext()).startDownloadManager();
          waitingForSelection = true;
          return;
        }
        ((BRouterActivity) getContext()).selectProfile(profiles.toArray(new String[0]), true);
      } catch (Exception e) {
        String msg = e instanceof IllegalArgumentException ? e.getMessage()
          + (cor == null ? "" : " (coordinate-source: " + cor.basedir + cor.rootdir + ")") : e.toString();
  
        AppLogger.log(msg);
        AppLogger.log(AppLogger.formatThrowable(e));
  
        ((BRouterActivity) getContext()).showErrorMessage(msg + "\n" + AppLogger.formatThrowable(e));
      }
      waitingForSelection = true;
    }
  
    private void populateBasedir(String basedir) {
      String version = "v" + getContext().getString(R.string.app_version);
  
      // create missing directories
      assertDirectoryExists("project directory", new File(basedir, "brouter"), null, null);
      segmentDir = new File(basedir, "/brouter/segments4");
      if (assertDirectoryExists("data directory", segmentDir, "segments4.zip", null)) {
        ConfigMigration.tryMigrateStorageConfig(
          new File(basedir + "/brouter/segments3/storageconfig.txt"),
          new File(basedir + "/brouter/segments4/storageconfig.txt"));
      } else {
        ServerConfig.checkForUpdate(getContext(), segmentDir, "segments4.zip");
      }
      profileDir = new File(basedir, "brouter/profiles2");
      assertDirectoryExists("profile directory", profileDir, "profiles2.zip", version);
      modesDir = new File(basedir, "/brouter/modes");
      assertDirectoryExists("modes directory", modesDir, "modes.zip", version);
      assertDirectoryExists("readmes directory", new File(basedir, "brouter/readmes"), "readmes.zip", version);
  
      File inputDir = new File(basedir, "brouter/import");
      assertDirectoryExists("input directory", inputDir, null, version);
    }
  
    private void moveFolders(String oldMigrationPath, String basedir) {
      File oldDir = new File(oldMigrationPath);
      File[] oldFiles = oldDir.listFiles();
      if (oldFiles != null) {
        for (File f : oldFiles) {
          if (f.isDirectory()) {
            int index = f.getAbsolutePath().lastIndexOf("/");
            String tmpdir = basedir + f.getAbsolutePath().substring(index);
            moveFolders(f.getAbsolutePath(), tmpdir);
          } else {
            if (!f.getName().startsWith("v1.6")) {
              moveFile(oldMigrationPath, f.getName(), basedir);
            }
          }
  
        }
      }
    }
  
    private void copyFile(String inputPath, String inputFile, String outputPath) {
      InputStream in;
      OutputStream out;
  
      try {
        //create output directory if it doesn't exist
        File dir = new File(outputPath);
        if (!dir.exists()) {
          dir.mkdirs();
        }
  
        in = new FileInputStream(new File(inputPath, inputFile));
        out = new FileOutputStream(new File(outputPath, inputFile));
  
        byte[] buffer = new byte[1024];
        int read;
        while ((read = in.read(buffer)) != -1) {
          out.write(buffer, 0, read);
        }
        in.close();
  
        // write the output file
        out.flush();
        out.close();
  
      } catch (FileNotFoundException fileNotFoundException) {
        Log.e(TAG, fileNotFoundException.getMessage());
      } catch (Exception e) {
        Log.e(TAG, e.getMessage());
      }
    }
  
    private void moveFile(String inputPath, String inputFile, String outputPath) {
      copyFile(inputPath, inputFile, outputPath);
      // delete the original file
      new File(inputPath, inputFile).delete();
    }
  
    public boolean hasUpToDateLookups() {
      BExpressionMetaData meta = new BExpressionMetaData();
      meta.readMetaData(new File(profileDir, "lookups.dat"));
      return meta.lookupVersion == 10;
    }
  
    public void continueProcessing() {
      waitingForSelection = false;
      invalidate();
    }
  
    public void updateViaList(Set<String> selectedVias) {
      ArrayList<OsmNodeNamed> filtered = new ArrayList<>(wpList.size());
      for (OsmNodeNamed n : wpList) {
        String name = n.name;
        if ("from".equals(name) || "to".equals(name) || selectedVias.contains(name))
          filtered.add(n);
      }
      wpList = filtered;
    }
  
    public void updateNogoList(boolean[] enabled) {
      for (int i = nogoList.size() - 1; i >= 0; i--) {
        if (enabled[i]) {
          nogoVetoList.add(nogoList.get(i));
          nogoList.remove(i);
        }
      }
    }
  
    public void pickWaypoints() {
      String msg = null;
  
      if (cor.allpoints == null) {
        try {
          cor.readAllPoints();
        } catch (Exception e) {
          msg = getContext().getString(R.string.msg_read_wpt_error)+  ": " + e;
        }
  
        int size = cor.allpoints.size();
        if (size < 1)
          msg = getContext().getString(R.string.msg_no_wpt);
        if (size > 1000)
          msg = String.format(getContext().getString(R.string.msg_too_much_wpts), size);
      }
  
      if (msg != null) {
        ((BRouterActivity) getContext()).showErrorMessage(msg);
      } else {
        String[] wpts = new String[cor.allpoints.size()];
        int i = 0;
        for (OsmNodeNamed wp : cor.allpoints)
          wpts[i++] = wp.name;
        ((BRouterActivity) getContext()).selectWaypoint(wpts);
      }
    }
  
    public void updateWaypointList(String waypoint) {
      for (OsmNodeNamed wp : cor.allpoints) {
        if (wp.name.equals(waypoint)) {
          if (wp.ilat != 0 || wp.ilon != 0) {
            int nwp = wpList.size();
            if (nwp == 0 || wpList.get(nwp - 1) != wp) {
              wpList.add(wp);
            }
          }
          return;
        }
      }
    }
  
    public void finishWaypointSelection() {
      needsWaypointSelection = false;
    }
  
    private List<OsmNodeNamed> readWpList(BufferedReader br, boolean isNogo) throws Exception {
      int cnt = Integer.parseInt(br.readLine());
      List<OsmNodeNamed> res = new ArrayList<>(cnt);
      for (int i = 0; i < cnt; i++) {
        OsmNodeNamed wp = OsmNodeNamed.decodeNogo(br.readLine());
        wp.isNogo = isNogo;
        res.add(wp);
      }
      return res;
    }
  
    public void startProcessing(String profile) {
      rawTrackPath = null;
      String params = null;
      if (profile.startsWith("<repeat")) {
        needsViaSelection = needsNogoSelection = needsWaypointSelection = false;
        try {
          File lastTimeoutFile = new File(modesDir + "/timeoutdata.txt");
          BufferedReader br = new BufferedReader(new FileReader(lastTimeoutFile));
          profile = br.readLine();
          rawTrackPath = br.readLine();
          wpList = readWpList(br, false);
          nogoList = readWpList(br, true);
          params = br.readLine();
          br.close();
        } catch (Exception e) {
          AppLogger.log(AppLogger.formatThrowable(e));
          ((BRouterActivity) getContext()).showErrorMessage(e.toString());
        }
      } else if ("remote".equals(profileName)) {
        rawTrackPath = modesDir + "/remote_rawtrack.dat";
      }
  
      String profilePath = profileDir + "/" + profile + ".brf";
      profileName = profile;
  
      if (needsViaSelection) {
        needsViaSelection = false;
        String[] availableVias = new String[wpList.size() - 2];
        for (int viaidx = 0; viaidx < wpList.size() - 2; viaidx++)
          availableVias[viaidx] = wpList.get(viaidx + 1).name;
        ((BRouterActivity) getContext()).selectVias(availableVias);
        return;
      }
  
      if (needsNogoSelection) {
        needsNogoSelection = false;
        ((BRouterActivity) getContext()).selectNogos(nogoList);
        return;
      }
  
      if (needsWaypointSelection) {
        StringBuilder msg;
        if (wpList.size() == 0) {
          msg = new StringBuilder(getContext().getString(R.string.msg_no_wpt_selection) + "(coordinate-source: " + cor.basedir + cor.rootdir + ")");
        } else {
          msg = new StringBuilder(getContext().getString(R.string.msg_wpt_selection));
          for (int i = 0; i < wpList.size(); i++)
            msg.append(i > 0 ? "->" : "").append(wpList.get(i).name);
        }
        ((BRouterActivity) getContext()).showResultMessage(getContext().getString(R.string.title_action), msg.toString(), wpList.size());
        return;
      }
  
      try {
        waitingForSelection = false;
  
        RoutingContext rc = new RoutingContext();
  
        rc.localFunction = profilePath;
        rc.turnInstructionMode = cor.getTurnInstructionMode();
  
        if (params != null && params.length() > 2) {
          try {
            Map<String, String> profileParamsCollection = null;
            RoutingParamCollector routingParamCollector = new RoutingParamCollector();
            profileParamsCollection = routingParamCollector.getUrlParams(params);
            routingParamCollector.setProfileParams(rc, profileParamsCollection);
          } catch (Exception e) {}
        }
  
        int plain_distance = 0;
        int maxlon = Integer.MIN_VALUE;
        int minlon = Integer.MAX_VALUE;
        int maxlat = Integer.MIN_VALUE;
        int minlat = Integer.MAX_VALUE;
  
        OsmNode prev = null;
        for (OsmNode n : wpList) {
          maxlon = Math.max(n.ilon, maxlon);
          minlon = Math.min(n.ilon, minlon);
          maxlat = Math.max(n.ilat, maxlat);
          minlat = Math.min(n.ilat, minlat);
          if (prev != null) {
            plain_distance += n.calcDistance(prev);
          }
          prev = n;
        }
        toast("Plain distance = " + plain_distance / 1000. + " km");
  
        centerLon = (maxlon + minlon) / 2;
        centerLat = (maxlat + minlat) / 2;
  
        double[] lonlat2m = CheapRuler.getLonLatToMeterScales(centerLat);
        double dlon2m = lonlat2m[0];
        double dlat2m = lonlat2m[1];
        double difflon = (maxlon - minlon) * dlon2m;
        double difflat = (maxlat - minlat) * dlat2m;
  
        scaleLon = imgw / (difflon * 1.5);
        scaleLat = imgh / (difflat * 1.5);
        scaleMeter2Pixel = Math.min(scaleLon, scaleLat);
        scaleLon = scaleMeter2Pixel * dlon2m;
        scaleLat = scaleMeter2Pixel * dlat2m;
  
        startTime = System.currentTimeMillis();
        RoutingContext.prepareNogoPoints(nogoList);
        rc.nogopoints = nogoList;
  
        rc.memoryclass = memoryClass;
        if (memoryClass < 16) {
          rc.memoryclass = 16;
        } else if (memoryClass > 256) {
          rc.memoryclass = 256;
        }
  
  
        // for profile remote, use ref-track logic same as service interface
        rc.rawTrackPath = rawTrackPath;
  
        cr = new RoutingEngine(tracksDir.getAbsolutePath() + "/brouter", null, segmentDir, wpList, rc);
        cr.start();
        invalidate();
  
      } catch (Exception e) {
        String msg = e instanceof IllegalArgumentException ? e.getMessage() : e.toString();
        toast(msg);
      }
    }
  
    private boolean assertDirectoryExists(String message, File path, String assetZip, String versionTag) {
      boolean exists = path.exists();
      if (!exists) {
        path.mkdirs();
      }
      if (versionTag != null) {
        File vtag = new File(path, versionTag);
        try {
          exists = !vtag.createNewFile();
        } catch (IOException ignored) {
        } // well..
      }
  
      if (!exists) {
        // default contents from assets archive
        if (assetZip != null) {
          try {
            AssetManager assetManager = getContext().getAssets();
            InputStream is = assetManager.open(assetZip);
            ZipInputStream zis = new ZipInputStream(is);
            byte[] data = new byte[1024];
            for (; ; ) {
              ZipEntry ze = zis.getNextEntry();
              if (ze == null)
                break;
              if (ze.isDirectory()) {
                continue;
              }
              String name = ze.getName();
              File outfile = new File(path, name);
              String canonicalPath = outfile.getCanonicalPath();
              if (canonicalPath.startsWith(path.getCanonicalPath()) &&
                !outfile.exists() &&
                outfile.getParentFile() != null) {
                outfile.getParentFile().mkdirs();
                FileOutputStream fos = new FileOutputStream(outfile);
  
                for (; ; ) {
                  int len = zis.read(data, 0, 1024);
                  if (len < 0)
                    break;
                  fos.write(data, 0, len);
                }
                fos.close();
              }
            }
            zis.close();
            is.close();
            return true;
          } catch (IOException io) {
            throw new RuntimeException("error expanding " + assetZip + ": " + io);
          }
  
        }
      }
  
      if (!path.exists() || !path.isDirectory())
        throw new IllegalArgumentException(message + ": " + path + " cannot be created");
      return false;
    }
  
    private void paintPosition(int ilon, int ilat, int color, int with) {
      int lon = ilon - centerLon;
      int lat = ilat - centerLat;
      int x = imgw / 2 + (int) (scaleLon * lon);
      int y = imgh / 2 - (int) (scaleLat * lat);
      for (int nx = x - with; nx <= x + with; nx++)
        for (int ny = y - with; ny <= y + with; ny++) {
          if (nx >= 0 && nx < imgw && ny >= 0 && ny < imgh) {
            imgPixels[nx + imgw * ny] = color;
          }
        }
    }
  
    private void paintCircle(Canvas canvas, OsmNodeNamed n, int color, int minradius) {
      int lon = n.ilon - centerLon;
      int lat = n.ilat - centerLat;
      int x = imgw / 2 + (int) (scaleLon * lon);
      int y = imgh / 2 - (int) (scaleLat * lat);
  
      int ir = (int) (n.radius * scaleMeter2Pixel);
      if (ir > minradius) {
        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle((float) x, (float) y, (float) ir, paint);
      }
    }
  
    private void paintLine(Canvas canvas, final int ilon0, final int ilat0, final int ilon1, final int ilat1, final Paint paint) {
      final int lon0 = ilon0 - centerLon;
      final int lat0 = ilat0 - centerLat;
      final int lon1 = ilon1 - centerLon;
      final int lat1 = ilat1 - centerLat;
      final int x0 = imgw / 2 + (int) (scaleLon * lon0);
      final int y0 = imgh / 2 - (int) (scaleLat * lat0);
      final int x1 = imgw / 2 + (int) (scaleLon * lon1);
      final int y1 = imgh / 2 - (int) (scaleLat * lat1);
      canvas.drawLine((float) x0, (float) y0, (float) x1, (float) y1, paint);
    }
  
    private void paintPolygon(Canvas canvas, OsmNogoPolygon p, int minradius) {
      final int ir = (int) (p.radius * scaleMeter2Pixel);
      if (ir > minradius) {
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
  
        Point p0 = p.isClosed ? p.points.get(p.points.size() - 1) : null;
  
        for (final Point p1 : p.points) {
          if (p0 != null) {
            paintLine(canvas, p0.x, p0.y, p1.x, p1.y, paint);
          }
          p0 = p1;
        }
      }
    }
  
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
      imgw = w;
      imgh = h;
    }
  
    private void toast(String msg) {
      Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
      lastDataTime += 4000; // give time for the toast before exiting
    }
  
    @Override
    protected void onDraw(Canvas canvas) {
      try {
        _onDraw(canvas);
      } catch (Throwable t) {
        // on out of mem, try to stop the show
        if (cr != null)
          cr.cleanOnOOM();
        cr = null;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException ignored) {
        }
        ((BRouterActivity) getContext()).showErrorMessage(t.toString());
        waitingForSelection = true;
      }
    }
  
    private void _onDraw(Canvas canvas) {
      if (waitingForSelection)
        return;
  
      long currentTs = System.currentTimeMillis();
      long diffTs = currentTs - lastTs;
      long sleeptime = 500 - diffTs;
      while (sleeptime < 200)
        sleeptime += 500;
  
      try {
        Thread.sleep(sleeptime);
      } catch (InterruptedException ignored) {
      }
      lastTs = System.currentTimeMillis();
  
      if (cr == null || cr.isFinished()) {
        if (cr != null) {
          if (cr.getErrorMessage() != null) {
            ((BRouterActivity) getContext()).showErrorMessage(cr.getErrorMessage());
          } else {
            String memstat = memoryClass + "mb pathPeak " + ((cr.getPathPeak() + 500) / 1000) + "k";
            String result = String.format(getContext().getString(R.string.msg_status_result),
              getContext().getString(R.string.app_version),
              memstat,
              Double.toString(cr.getDistance() / 1000.),
              Integer.toString(cr.getAscend()),
              Integer.toString(cr.getPlainAscend()),
              cr.getTime());
  
            rawTrack = cr.getFoundRawTrack();
  
            // for profile "remote", always persist referencetrack
            if (cr.getAlternativeIndex() == 0 && rawTrackPath != null) {
              writeRawTrackToPath(rawTrackPath);
            }
  
            String title = getContext().getString(R.string.success);
            if (cr.getAlternativeIndex() > 0)
              title += " / " + cr.getAlternativeIndex() + ". " + getContext().getString(R.string.msg_alternative);
  
            ((BRouterActivity) getContext()).showResultMessage(title, result, rawTrackPath == null ? -1 : -3);
            trackOutfile = cr.getOutfile();
          }
          cr = null;
          waitingForSelection = true;
          return;
        } else if (System.currentTimeMillis() > lastDataTime) {
          System.exit(0);
        }
      } else {
        lastDataTime = System.currentTimeMillis();
        imgPixels = new int[imgw * imgh];
  
        int[] openSet = cr.getOpenSet();
        for (int si = 0; si < openSet.length; si += 2) {
          paintPosition(openSet[si], openSet[si + 1], 0xffffff, 1);
        }
        // paint nogos on top (red)
        int minradius = 4;
        for (int ngi = 0; ngi < nogoList.size(); ngi++) {
          OsmNodeNamed n = nogoList.get(ngi);
          int color = 0xff0000;
          paintPosition(n.ilon, n.ilat, color, minradius);
        }
  
        // paint start/end/vias on top (yellow/green/blue)
        for (int wpi = 0; wpi < wpList.size(); wpi++) {
          OsmNodeNamed n = wpList.get(wpi);
          int color = wpi == 0 ? 0xffff00 : wpi < wpList.size() - 1 ? 0xff : 0xff00;
          paintPosition(n.ilon, n.ilat, color, minradius);
        }
  
        Bitmap bmp = Bitmap.createBitmap(imgPixels, imgw, imgh, Bitmap.Config.RGB_565);
        canvas.drawBitmap(bmp, 0, 0, null);
  
        // nogo circles if any
        for (int ngi = 0; ngi < nogoList.size(); ngi++) {
          OsmNodeNamed n = nogoList.get(ngi);
          if (n instanceof OsmNogoPolygon) {
            paintPolygon(canvas, (OsmNogoPolygon) n, minradius);
          } else {
            int color = Color.RED;
            paintCircle(canvas, n, color, minradius);
          }
        }
  
        Paint paint = new Paint();
        paint.setColor(Color.WHITE);
        paint.setTextSize(20);
  
        long mseconds = System.currentTimeMillis() - startTime;
        long links = cr.getLinksProcessed();
        long perS = (1000 * links) / mseconds;
        String msg = "Links: " + cr.getLinksProcessed() + " in " + (mseconds / 1000) + "s (" + perS + " l/s)";
  
        canvas.drawText(msg, 10, 25, paint);
      }
      // and make sure to redraw asap
      invalidate();
    }
  
    private void writeRawTrackToMode(String mode) {
      writeRawTrackToPath(modesDir + "/" + mode + "_rawtrack.dat");
    }
  
    private void writeRawTrackToPath(String rawTrackPath) {
      if (rawTrack != null) {
        try {
          rawTrack.writeBinary(rawTrackPath);
        } catch (Exception ignored) {
        }
      } else {
        new File(rawTrackPath).delete();
      }
    }
  
    public void startConfigureService(String sparams) {
      String[] modes = new String[]
        {"foot_short", "foot_fast", "bicycle_short", "bicycle_fast", "motorcar_short", "motorcar_fast"};
      boolean[] modesChecked = new boolean[6];
  
      String msg = "Choose service-modes to configure (" + profileName + " [" + nogoVetoList.size() + "])";
  
      ((BRouterActivity) getContext()).selectRoutingModes(modes, modesChecked, msg, sparams);
    }
  
    public void configureService(String[] routingModes, boolean[] checkedModes, String sparams) {
      // read in current config
      TreeMap<String, ServiceModeConfig> map = new TreeMap<>();
      BufferedReader br = null;
      String modesFile = modesDir + "/serviceconfig.dat";
      try {
        br = new BufferedReader(new FileReader(modesFile));
        for (; ; ) {
          String line = br.readLine();
          if (line == null)
            break;
          ServiceModeConfig smc = new ServiceModeConfig(line);
          map.put(smc.mode, smc);
        }
      } catch (Exception ignored) {
      } finally {
        if (br != null)
          try {
            br.close();
          } catch (Exception ignored) {
          }
      }
  
      // replace selected modes
      for (int i = 0; i < 6; i++) {
        if (checkedModes[i]) {
          writeRawTrackToMode(routingModes[i]);
          ServiceModeConfig sm = map.get(routingModes[i]);
          String s = null;
          String p = null;
          if (sm != null) {
            s = sm.params;
            p = sm.profile;
          }
          if (!p.equals(profileName)) s = sparams;
          if (s == null || s.equals("")) s = "noparams";
          ServiceModeConfig smc = new ServiceModeConfig(routingModes[i], profileName, s);
          for (OsmNodeNamed nogo : nogoVetoList) {
            smc.nogoVetos.add(nogo.ilon + "," + nogo.ilat);
          }
          map.put(smc.mode, smc);
        }
      }
  
      // no write new config
      BufferedWriter bw = null;
      StringBuilder msg = new StringBuilder("Mode mapping is now:\n");
      msg.append("( [");
      msg.append(nogoVetoList.size() > 0 ? nogoVetoList.size() : "..").append("] counts nogo-vetos)\n");
      try {
        bw = new BufferedWriter(new FileWriter(modesFile));
        for (ServiceModeConfig smc : map.values()) {
          bw.write(smc.toLine());
          bw.write('\n');
          msg.append(smc).append('\n');
        }
      } catch (Exception ignored) {
      } finally {
        if (bw != null)
          try {
            bw.close();
          } catch (Exception ignored) {
          }
      }
      ((BRouterActivity) getContext()).showModeConfigOverview(msg.toString());
    }
  
    public void configureServiceParams(String profile, String sparams) {
      List<ServiceModeConfig> map = new ArrayList<>();
      BufferedReader br = null;
      String modesFile = modesDir + "/serviceconfig.dat";
      try {
        br = new BufferedReader(new FileReader(modesFile));
        for (; ; ) {
          String line = br.readLine();
          if (line == null)
            break;
          ServiceModeConfig smc = new ServiceModeConfig(line);
          if (smc.profile.equals(profile)) smc.params = sparams;
          map.add(smc);
        }
      } catch (Exception ignored) {
      } finally {
        if (br != null)
          try {
            br.close();
          } catch (Exception ignored) {
          }
      }
  
      // now write new config
      BufferedWriter bw = null;
      StringBuilder msg = new StringBuilder("Mode mapping is now:\n");
      msg.append("( [");
      msg.append(nogoVetoList.size() > 0 ? nogoVetoList.size() : "..").append("] counts nogo-vetos)\n");
      try {
        bw = new BufferedWriter(new FileWriter(modesFile));
        for (ServiceModeConfig smc : map) {
          bw.write(smc.toLine());
          bw.write('\n');
          msg.append(smc).append('\n');
        }
      } catch (Exception ignored) {
      } finally {
        if (bw != null)
          try {
            bw.close();
          } catch (Exception ignored) {
          }
      }
      ((BRouterActivity) getContext()).showModeConfigOverview(msg.toString());
    }
  
    public String getConfigureServiceParams(String profile) {
      List<ServiceModeConfig> map = new ArrayList<>();
      BufferedReader br = null;
      String modesFile = modesDir + "/serviceconfig.dat";
      try {
        br = new BufferedReader(new FileReader(modesFile));
        for (; ; ) {
          String line = br.readLine();
          if (line == null)
            break;
          ServiceModeConfig smc = new ServiceModeConfig(line);
          if (smc.profile.equals(profile)) {
            if (!smc.params.equals("noparams")) return smc.params;
            else return "";
          }
          map.add(smc);
        }
      } catch (Exception ignored) {
      } finally {
        if (br != null)
          try {
            br.close();
          } catch (Exception ignored) {
          }
      }
      // no profile found
      return null;
    }
  
    public void shareTrack() {
      File track = new File(trackOutfile);
      // Copy file to cache to ensure FileProvider allows sharing the file
      File cacheDir = getContext().getCacheDir();
      copyFile(track.getParent(), track.getName(), cacheDir.getAbsolutePath());
      Intent intent = new Intent();
      intent.setDataAndType(FileProvider.getUriForFile(getContext(), "btools.routing.fileprovider", new File(cacheDir, track.getName())),
        "application/gpx+xml");
      intent.setAction(Intent.ACTION_VIEW);
      intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
      getContext().startActivity(intent);
    }
  }
  