kopia lustrzana https://github.com/ge0rg/aprsdroid
465 wiersze
14 KiB
Scala
465 wiersze
14 KiB
Scala
package org.aprsdroid.app
|
|
|
|
import _root_.android.Manifest
|
|
import _root_.android.app.AlertDialog
|
|
import _root_.android.content.{DialogInterface, Intent, IntentFilter}
|
|
import _root_.android.content.pm.PackageManager
|
|
import _root_.android.content.res.Configuration
|
|
import _root_.android.database.Cursor
|
|
import _root_.android.graphics.drawable.{BitmapDrawable, Drawable}
|
|
import _root_.android.graphics.{Canvas, Paint, Path, Point, Rect, Typeface}
|
|
import _root_.android.os.{Build, Bundle}
|
|
import _root_.android.util.Log
|
|
import _root_.android.view.{KeyEvent, Menu, MenuItem, View}
|
|
import _root_.android.widget.Toast
|
|
import _root_.org.mapsforge.v3.android.maps._
|
|
import _root_.org.mapsforge.v3.core.{GeoPoint, Tile}
|
|
import _root_.org.mapsforge.v3.android.maps.overlay.{ItemizedOverlay, OverlayItem}
|
|
|
|
import _root_.scala.collection.mutable.ArrayBuffer
|
|
import _root_.java.io.File
|
|
import _root_.java.util.ArrayList
|
|
import java.lang.UnsupportedOperationException
|
|
|
|
import org.mapsforge.v3.android.maps.mapgenerator.{MapGeneratorFactory, MapGeneratorInternal}
|
|
import org.mapsforge.v3.map.reader.header.FileOpenResult
|
|
|
|
// to make scala-style iterating over arraylist possible
|
|
import scala.collection.JavaConversions._
|
|
|
|
class MapAct extends MapActivity with MapMenuHelper {
|
|
override val TAG = "APRSdroid.Map"
|
|
|
|
menu_id = R.id.map
|
|
|
|
lazy val mapview = findViewById(R.id.mapview).asInstanceOf[MapView]
|
|
lazy val allicons = this.getResources().getDrawable(R.drawable.allicons)
|
|
lazy val db = StorageDatabase.open(this)
|
|
lazy val staoverlay = new StationOverlay(allicons, this, db)
|
|
lazy val loading = findViewById(R.id.loading).asInstanceOf[View]
|
|
lazy val locReceiver = new LocationReceiver2[ArrayList[OSMStation]](staoverlay.load_stations,
|
|
staoverlay.replace_stations, staoverlay.cancel_stations)
|
|
|
|
override def onCreate(savedInstanceState: Bundle) {
|
|
super.onCreate(savedInstanceState)
|
|
setContentView(R.layout.mapview)
|
|
mapview.setBuiltInZoomControls(true)
|
|
mapview.getOverlays().add(staoverlay)
|
|
mapview.setTextScale(getResources().getDisplayMetrics().density)
|
|
|
|
startLoading()
|
|
}
|
|
|
|
override def onResume() {
|
|
super.onResume()
|
|
// only make it default if not tracking
|
|
if (isCoordinateChooser)
|
|
setTitle(R.string.p_source_from_map)
|
|
else if (targetcall == "")
|
|
makeLaunchActivity("map")
|
|
else
|
|
setLongTitle(R.string.app_map, targetcall)
|
|
setKeepScreenOn()
|
|
setVolumeControls()
|
|
//checkPermissions(Array(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), RELOAD_MAP)
|
|
reloadMapAndTheme()
|
|
mapview.requestFocus()
|
|
}
|
|
|
|
override def onConfigurationChanged(c : Configuration) = {
|
|
super.onConfigurationChanged(c)
|
|
if (targetcall != "")
|
|
setLongTitle(R.string.app_map, targetcall)
|
|
}
|
|
|
|
override def onPause() {
|
|
super.onPause()
|
|
val pos = mapview.getMapPosition().getMapPosition()
|
|
if (pos == null || pos.geoPoint == null)
|
|
return
|
|
saveMapViewPosition(pos.geoPoint.latitudeE6/1000000.0f, pos.geoPoint.longitudeE6/1000000.0f, pos.zoomLevel)
|
|
}
|
|
|
|
override def onDestroy() {
|
|
super.onDestroy()
|
|
unregisterReceiver(locReceiver)
|
|
}
|
|
|
|
override def loadMapViewPosition(lat : Float, lon : Float, zoom : Float) {
|
|
mapview.getController().setCenter(new GeoPoint(lat, lon))
|
|
mapview.getController().setZoom(zoom.asInstanceOf[Int])
|
|
}
|
|
|
|
def startLoading() {
|
|
registerReceiver(locReceiver, new IntentFilter(AprsService.UPDATE))
|
|
locReceiver.startTask(null)
|
|
}
|
|
|
|
val RELOAD_MAP = 1010
|
|
|
|
override def getActionName(action : Int): Int = {
|
|
action match {
|
|
case RELOAD_MAP => R.string.show_map
|
|
case _ => super.getActionName(action)
|
|
}
|
|
}
|
|
override def onAllPermissionsGranted(action: Int): Unit = {
|
|
action match {
|
|
case RELOAD_MAP => reloadMapAndTheme()
|
|
case _ => super.onAllPermissionsGranted(action)
|
|
}
|
|
}
|
|
|
|
override def onPermissionsFailed(action : Int, permissions : Set[String]): Unit = {
|
|
// fail to online OSM map
|
|
}
|
|
|
|
override def onPermissionsFailedCancel(action: Int): Unit = {
|
|
// should never be called
|
|
}
|
|
|
|
def reloadMapAndTheme() {
|
|
val mapfile = new File(prefs.getString("mapfile", android.os.Environment.getExternalStorageDirectory() + "/aprsdroid.map"))
|
|
var error = if (mapfile.exists() && mapfile.canRead()) {
|
|
val result = mapview.setMapFile(mapfile)
|
|
// output map loader's error if loading failed
|
|
if (result.isSuccess) null else result.getErrorMessage
|
|
} else if (prefs.getString("mapfile", null) != null) {
|
|
// output generic error if file was configured but is not loadable
|
|
getString(R.string.mapfile_error, mapfile)
|
|
} else {
|
|
// do not output error if no map file was configured, silently load online osm
|
|
null
|
|
}
|
|
if (error != null)
|
|
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
|
|
// all map file attempts failed, fall back to online
|
|
try {
|
|
if (mapview.getMapFile == null) {
|
|
val map_source = MapGeneratorInternal.MAPNIK
|
|
val map_gen = new OsmTileDownloader()
|
|
map_gen.setUserAgent(getString(R.string.build_version))
|
|
mapview.setMapGenerator(map_gen)
|
|
}
|
|
} catch {
|
|
case _ : UnsupportedOperationException => /* ignore, this is thrown by online map generator */
|
|
}
|
|
val themefile = new File(prefs.getString("themefile", android.os.Environment.getExternalStorageDirectory() + "/aprsdroid.xml"))
|
|
if (themefile.exists())
|
|
mapview.setRenderTheme(themefile)
|
|
loadMapViewPosition()
|
|
}
|
|
|
|
override def onKeyDown(keyCode : Int, event : KeyEvent) : Boolean = {
|
|
keyCode match {
|
|
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD |
|
|
KeyEvent.KEYCODE_MEDIA_NEXT =>
|
|
changeZoom(+1)
|
|
true
|
|
case KeyEvent.KEYCODE_MEDIA_REWIND |
|
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS =>
|
|
changeZoom(-1)
|
|
true
|
|
case KeyEvent.KEYCODE_MEDIA_PLAY |
|
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE =>
|
|
if (mapview.hasFocus())
|
|
mapview.focusSearch(View.FOCUS_FORWARD).requestFocus()
|
|
else
|
|
mapview.requestFocus()
|
|
true
|
|
case KeyEvent.KEYCODE_DPAD_CENTER |
|
|
KeyEvent.KEYCODE_ENTER =>
|
|
// TODO: return coordinates
|
|
if (isCoordinateChooser) {
|
|
setResult(android.app.Activity.RESULT_OK, resultIntent)
|
|
finish()
|
|
}
|
|
true
|
|
case _ => super.onKeyDown(keyCode, event)
|
|
}
|
|
}
|
|
|
|
def updateCoordinateInfo(): Unit = {
|
|
if (!isCoordinateChooser)
|
|
return
|
|
val pos = mapview.getMapPosition().getMapPosition()
|
|
if (pos == null || pos.geoPoint == null)
|
|
return
|
|
updateCoordinateInfo(pos.geoPoint.latitudeE6/1000000.0f, pos.geoPoint.longitudeE6/1000000.0f)
|
|
}
|
|
|
|
override def changeZoom(delta : Int) {
|
|
mapview.getController().setZoom(mapview.getMapPosition().getZoomLevel() + delta)
|
|
}
|
|
|
|
def animateToCall() {
|
|
if (targetcall != "") {
|
|
val (found, lat, lon) = getStaPosition(db, targetcall)
|
|
if (found)
|
|
mapview.getController().setCenter(new GeoPoint(lat, lon))
|
|
}
|
|
}
|
|
|
|
def onPostLoad() {
|
|
mapview.invalidate()
|
|
onStopLoading()
|
|
animateToCall()
|
|
}
|
|
|
|
override def reloadMap() {
|
|
onStartLoading()
|
|
locReceiver.startTask(null)
|
|
}
|
|
|
|
override def onStartLoading() {
|
|
loading.setVisibility(View.VISIBLE)
|
|
}
|
|
|
|
override def onStopLoading() {
|
|
loading.setVisibility(View.GONE)
|
|
}
|
|
}
|
|
|
|
class OSMStation(val movelog : ArrayBuffer[GeoPoint], val pt : GeoPoint,
|
|
val call : String, val origin : String, val symbol : String)
|
|
extends OverlayItem(pt, call, origin) {
|
|
|
|
def inArea(bl : GeoPoint, tr : GeoPoint) = {
|
|
val lat_ok = (bl.latitudeE6 <= pt.latitudeE6 && pt.latitudeE6 <= tr.latitudeE6)
|
|
val lon_ok = if (bl.longitudeE6 <= tr.longitudeE6)
|
|
(bl.longitudeE6 <= pt.longitudeE6 && pt.longitudeE6 <= tr.longitudeE6)
|
|
else
|
|
(bl.longitudeE6 <= pt.longitudeE6 || pt.longitudeE6 <= tr.longitudeE6)
|
|
lat_ok && lon_ok
|
|
}
|
|
}
|
|
|
|
class StationOverlay(icons : Drawable, context : MapAct, db : StorageDatabase) extends ItemizedOverlay[OSMStation](icons) {
|
|
val TAG = "APRSdroid.StaOverlay"
|
|
|
|
//lazy val calls = new scala.collection.mutable.HashMap[String, Boolean]()
|
|
var stations = new java.util.ArrayList[OSMStation]()
|
|
|
|
// prevent android bug #11666
|
|
populate()
|
|
|
|
val iconbitmap = icons.asInstanceOf[BitmapDrawable].getBitmap
|
|
val symbolSize = iconbitmap.getWidth()/16
|
|
lazy val drawSize = (context.getResources().getDisplayMetrics().density * 24).toInt
|
|
|
|
icons.setBounds(0, 0, symbolSize, symbolSize)
|
|
|
|
override def size() = stations.size()
|
|
override def createItem(idx : Int) : OSMStation = stations.get(idx)
|
|
|
|
def symbol2rect(index : Int, page : Int) : Rect = {
|
|
// check for overflow
|
|
if (index < 0 || index >= 6*16)
|
|
return new Rect(0, 0, symbolSize, symbolSize)
|
|
val alt_offset = page*symbolSize*6
|
|
val y = (index / 16) * symbolSize + alt_offset
|
|
val x = (index % 16) * symbolSize
|
|
new Rect(x, y, x+symbolSize, y+symbolSize)
|
|
}
|
|
def symbol2rect(symbol : String) : Rect = {
|
|
symbol2rect(symbol(1) - 33, if (symbol(0) == '/') 0 else 1)
|
|
}
|
|
|
|
def symbolIsOverlayed(symbol : String) = {
|
|
(symbol(0) != '/' && symbol(0) != '\\')
|
|
}
|
|
|
|
def drawTrace(c : Canvas, proj : Projection, s : OSMStation) : Unit = {
|
|
//Log.d(TAG, "drawing trace of %s".format(call))
|
|
|
|
val tracePaint = new Paint()
|
|
tracePaint.setARGB(128, 100, 100, 255)
|
|
tracePaint.setStyle(Paint.Style.STROKE)
|
|
tracePaint.setStrokeJoin(Paint.Join.ROUND)
|
|
tracePaint.setStrokeCap(Paint.Cap.ROUND)
|
|
tracePaint.setStrokeWidth(drawSize/6)
|
|
tracePaint.setAntiAlias(true)
|
|
|
|
val dotPaint = new Paint()
|
|
dotPaint.setARGB(128, 255, 0, 0)
|
|
dotPaint.setStyle(Paint.Style.FILL)
|
|
dotPaint.setAntiAlias(true)
|
|
|
|
|
|
val path = new Path()
|
|
val point = new Point()
|
|
|
|
if (s.movelog.size() < 2) {
|
|
return
|
|
}
|
|
var first = true
|
|
for (p <- s.movelog) {
|
|
proj.toPixels(p, point)
|
|
if (first) {
|
|
path.moveTo(point.x, point.y)
|
|
first = false
|
|
} else
|
|
path.lineTo(point.x, point.y)
|
|
c.drawCircle(point.x, point.y, drawSize/12, dotPaint)
|
|
}
|
|
c.drawPath(path, tracePaint)
|
|
}
|
|
|
|
override def drawOverlayBitmap(c : Canvas, dp : Point, proj : Projection, zoom : Byte) : Unit = {
|
|
|
|
if (!context.mapview.getMapPosition.isValid)
|
|
return
|
|
Log.d(TAG, "draw: symbolSize=" + symbolSize + " drawSize=" + drawSize)
|
|
val fontSize = drawSize*7/8
|
|
val textPaint = new Paint()
|
|
textPaint.setColor(0xff000000)
|
|
textPaint.setTextAlign(Paint.Align.CENTER)
|
|
textPaint.setTextSize(fontSize)
|
|
textPaint.setTypeface(Typeface.MONOSPACE)
|
|
textPaint.setAntiAlias(true)
|
|
|
|
val symbPaint = new Paint(textPaint)
|
|
symbPaint.setARGB(255, 255, 255, 255)
|
|
symbPaint.setTextSize(drawSize*3/4 - 1)
|
|
|
|
val strokePaint = new Paint(textPaint)
|
|
strokePaint.setColor(0xffc8ffc8)
|
|
strokePaint.setStyle(Paint.Style.STROKE)
|
|
strokePaint.setStrokeWidth(drawSize.asInstanceOf[Float]/12.0f)
|
|
|
|
strokePaint.setShadowLayer(10, 0, 0, 0x80c8ffc8)
|
|
|
|
|
|
val p = new Point()
|
|
val (width, height) = (c.getWidth(), c.getHeight())
|
|
val ss = drawSize/2
|
|
for (s <- stations) {
|
|
proj.toPixels(s.pt, p)
|
|
if (p.x >= 0 && p.y >= 0 && p.x < width && p.y < height) {
|
|
val srcRect = symbol2rect(s.symbol)
|
|
val destRect = new Rect(p.x-ss, p.y-ss, p.x+ss, p.y+ss)
|
|
// first draw callsign and trace
|
|
if (zoom >= 10) {
|
|
drawTrace(c, proj, s)
|
|
|
|
c.drawText(s.call, p.x, p.y+ss+fontSize, strokePaint)
|
|
c.drawText(s.call, p.x, p.y+ss+fontSize, textPaint)
|
|
}
|
|
// then the bitmap
|
|
c.drawBitmap(iconbitmap, srcRect, destRect, null)
|
|
// and finally the bitmap overlay, if any
|
|
if (symbolIsOverlayed(s.symbol)) {
|
|
// use page 2, overlay letters
|
|
c.drawBitmap(iconbitmap, symbol2rect(s.symbol(0)-33, 2), destRect, null)
|
|
}
|
|
}
|
|
}
|
|
import AprsService.block2runnable
|
|
context.handler.post { context.updateCoordinateInfo() }
|
|
}
|
|
|
|
def addStation(sta : OSMStation) {
|
|
//if (calls.contains(sta.getTitle()))
|
|
// return
|
|
//calls.add(sta.getTitle(), true)
|
|
stations.add(sta)
|
|
}
|
|
|
|
override def onTap(gp : GeoPoint, mv : MapView) : Boolean = {
|
|
//Log.d(TAG, "user tapped " + gp)
|
|
//Log.d(TAG, "icon bounds: " + icons.getBounds())
|
|
// convert geopoint to pixels
|
|
val proj = mv.getProjection()
|
|
val p = proj.toPixels(gp, null)
|
|
// ... to pixel area ... to geo area
|
|
//Log.d(TAG, "coords: " + p)
|
|
val botleft = proj.fromPixels(p.x - 16, p.y + 16)
|
|
val topright = proj.fromPixels(p.x + 16, p.y - 16)
|
|
Log.d(TAG, "from " + botleft + " to " + topright)
|
|
// fetch stations in the tap
|
|
val list = stations.filter(_.inArea(botleft, topright)).map(_.call)
|
|
Log.d(TAG, "found " + list.size() + " stations")
|
|
val result = if (list.size() == 0)
|
|
false // nothing found, do not revert to superclass
|
|
else if (list.size() == 1) {
|
|
// found one entry
|
|
val call = list.get(0)
|
|
Log.d(TAG, "user clicked on " + call)
|
|
context.openDetails(call)
|
|
true
|
|
} else {
|
|
// TODO: replace simple adapter with StationListAdapter for better UI
|
|
new AlertDialog.Builder(context).setTitle(R.string.map_select)
|
|
.setItems(list.toArray.asInstanceOf[Array[CharSequence]], new DialogInterface.OnClickListener() {
|
|
override def onClick(di : DialogInterface, item : Int) {
|
|
context.openDetails(list.get(item))
|
|
}})
|
|
.setNegativeButton(android.R.string.cancel, null)
|
|
.show()
|
|
true
|
|
}
|
|
result
|
|
}
|
|
|
|
override def onTap(index : Int) : Boolean = {
|
|
val s = stations(index)
|
|
val target = if (s.origin != null && s.origin != "") s.origin
|
|
else s.call
|
|
Log.d(TAG, "user clicked on " + s.call + "/" + target)
|
|
context.openDetails(s.call)
|
|
true
|
|
}
|
|
|
|
def fetchStaPositions(call : String, c : Cursor) : ArrayBuffer[GeoPoint] = {
|
|
import StorageDatabase.Position._
|
|
val m = new ArrayBuffer[GeoPoint]()
|
|
// skip forward to the right callsign
|
|
while (!c.isAfterLast() && c.getString(COLUMN_CALL) < call)
|
|
c.moveToNext()
|
|
// add every matching entry to arraybuffer
|
|
while (!c.isAfterLast() && c.getString(COLUMN_CALL) == call) {
|
|
val lat = c.getInt(COLUMN_LAT)
|
|
val lon = c.getInt(COLUMN_LON)
|
|
m.add(new GeoPoint(lat, lon))
|
|
c.moveToNext()
|
|
}
|
|
m
|
|
}
|
|
|
|
def load_stations(i : Intent) : ArrayList[OSMStation] = {
|
|
import StorageDatabase.Station._
|
|
|
|
val s = new ArrayList[OSMStation]()
|
|
val age_ts = (System.currentTimeMillis - context.prefs.getShowAge()).toString
|
|
val filter = if (context.showObjects) "TS > ? OR CALL=?" else "(ORIGIN IS NULL AND TS > ?) OR CALL=?"
|
|
val c = db.getStations(filter, Array(age_ts, context.targetcall), null)
|
|
c.moveToFirst()
|
|
val pos_c = db.getAllStaPositions(age_ts)
|
|
pos_c.moveToFirst()
|
|
while (!c.isAfterLast()) {
|
|
val call = c.getString(COLUMN_MAP_CALL)
|
|
val lat = c.getInt(COLUMN_MAP_LAT)
|
|
val lon = c.getInt(COLUMN_MAP_LON)
|
|
val symbol = c.getString(COLUMN_MAP_SYMBOL)
|
|
val origin = c.getString(COLUMN_MAP_ORIGIN)
|
|
val p = new GeoPoint(lat, lon)
|
|
val m = fetchStaPositions(call, pos_c)
|
|
s.add(new OSMStation(m, p, call, origin, symbol))
|
|
c.moveToNext()
|
|
}
|
|
c.close()
|
|
pos_c.close()
|
|
Log.d(TAG, "total %d items".format(s.size()))
|
|
s
|
|
}
|
|
|
|
def replace_stations(s : ArrayList[OSMStation]) {
|
|
stations = s
|
|
Benchmark("populate") { populate() }
|
|
context.onPostLoad()
|
|
}
|
|
def cancel_stations(s : ArrayList[OSMStation]) {
|
|
}
|
|
|
|
}
|