OsmAnd/OsmAnd-telegram/src/net/osmand/telegram/utils/OsmandLocationUtils.kt

539 lines
18 KiB
Kotlin
Raw Normal View History

2019-01-25 17:04:26 +01:00
package net.osmand.telegram.utils
import android.os.AsyncTask
import net.osmand.Location
2019-02-01 12:34:22 +01:00
import net.osmand.data.LatLon
2019-01-25 17:04:26 +01:00
import net.osmand.telegram.TelegramApplication
2019-01-28 17:09:35 +01:00
import net.osmand.telegram.helpers.LocationMessages
import net.osmand.telegram.helpers.LocationMessages.BufferMessage
2019-01-29 18:03:44 +01:00
import net.osmand.telegram.helpers.LocationMessages.LocationMessage
2019-01-25 17:04:26 +01:00
import net.osmand.telegram.helpers.TelegramHelper
import net.osmand.telegram.helpers.TelegramUiHelper
import net.osmand.util.GeoPointParserUtil
import net.osmand.util.MapUtils
2019-01-25 17:04:26 +01:00
import org.drinkless.td.libcore.telegram.TdApi
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
2019-02-01 12:34:22 +01:00
const val TRACKS_DIR = "tracker/"
2019-01-25 17:04:26 +01:00
object OsmandLocationUtils {
const val DEVICE_PREFIX = "Device: "
const val LOCATION_PREFIX = "Location: "
const val LAST_LOCATION_PREFIX = "Last location: "
const val UPDATED_PREFIX = "Updated: "
const val USER_TEXT_LOCATION_TITLE = "\uD83D\uDDFA OsmAnd sharing:"
const val SHARING_LINK = "https://play.google.com/store/apps/details?id=net.osmand.telegram"
const val ALTITUDE_PREFIX = "Altitude: "
const val SPEED_PREFIX = "Speed: "
const val HDOP_PREFIX = "Horizontal precision: "
const val NOW = "now"
const val FEW_SECONDS_AGO = "few seconds ago"
const val SECONDS_AGO_SUFFIX = " seconds ago"
const val MINUTES_AGO_SUFFIX = " minutes ago"
const val HOURS_AGO_SUFFIX = " hours ago"
const val UTC_FORMAT_SUFFIX = " UTC"
val UTC_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
val UTC_TIME_FORMAT = SimpleDateFormat("HH:mm:ss", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
fun getLastUpdatedTime(message: TdApi.Message): Int {
val content = message.content
return when (content) {
is MessageOsmAndBotLocation -> content.lastUpdated
is MessageUserLocation -> content.lastUpdated
2019-01-25 17:04:26 +01:00
else -> Math.max(message.editDate, message.date)
}
}
fun getOsmAndBotDeviceName(message: TdApi.Message): String {
var deviceName = ""
if (message.replyMarkup is TdApi.ReplyMarkupInlineKeyboard) {
val replyMarkup = message.replyMarkup as TdApi.ReplyMarkupInlineKeyboard
try {
2019-02-08 18:12:17 +01:00
if (replyMarkup.rows[0].size > 1) {
deviceName = replyMarkup.rows[0][1].text.split("\\s".toRegex())[1]
} else if (message.content is TdApi.MessageText) {
deviceName = (message.content as TdApi.MessageText).text.text.lines().firstOrNull()?.removePrefix(DEVICE_PREFIX) ?: ""
}
2019-01-25 17:04:26 +01:00
} catch (e: Exception) {
}
}
return deviceName
}
2019-02-08 18:12:17 +01:00
fun parseMapLocation(message: TdApi.Message, botLocation: Boolean): MessageLocation {
val res = if (botLocation) MessageOsmAndBotLocation() else MessageUserLocation()
2019-01-25 17:04:26 +01:00
val messageLocation = message.content as TdApi.MessageLocation
2019-02-08 18:12:17 +01:00
res.apply {
2019-01-25 17:04:26 +01:00
lat = messageLocation.location.latitude
lon = messageLocation.location.longitude
lastUpdated = getLastUpdatedTime(message)
2019-02-08 18:12:17 +01:00
type = LocationMessages.TYPE_MAP
2019-01-25 17:04:26 +01:00
}
2019-02-08 18:12:17 +01:00
if (res is MessageOsmAndBotLocation) {
res.deviceName = getOsmAndBotDeviceName(message)
}
2019-02-08 18:12:17 +01:00
return res
}
2019-02-01 12:34:22 +01:00
fun parseMessage(message: TdApi.Message, helper: TelegramHelper, previousMessageLatLon: LatLon?): LocationMessage? {
2019-02-08 18:12:17 +01:00
val parsedContent = parseMessageContent(message, helper)
return createLocationMessage(message, helper, parsedContent, previousMessageLatLon)
}
2019-02-08 18:12:17 +01:00
fun parseMessageContent(message: TdApi.Message, helper: TelegramHelper): MessageLocation? {
val senderUserId = helper.getSenderMessageId(message)
val fromBot = helper.isOsmAndBot(senderUserId)
val viaBot = helper.isOsmAndBot(message.viaBotUserId)
return when (message.content) {
is TdApi.MessageText -> parseTextLocation((message.content as TdApi.MessageText).text, (fromBot || viaBot))
is TdApi.MessageLocation -> parseMapLocation(message, (fromBot || viaBot))
is MessageLocation -> message.content as MessageLocation
else -> null
2019-01-25 17:04:26 +01:00
}
2019-02-08 18:12:17 +01:00
}
2019-02-08 18:12:17 +01:00
fun createLocationMessage(message: TdApi.Message, helper: TelegramHelper, content:MessageLocation?, previousMessageLatLon: LatLon?):LocationMessage?{
if (content == null) {
return null
}
val senderUserId = helper.getSenderMessageId(message)
val messageType = getMessageType(message)
val distanceFromPrev = if (previousMessageLatLon != null) MapUtils.getDistance(previousMessageLatLon, content.lat, content.lon) else 0.0
val deviceName = if (content is MessageOsmAndBotLocation) content.deviceName else ""
return LocationMessage(senderUserId, message.chatId, content.lat, content.lon,
content.altitude, content.speed, content.hdop, content.bearing,
content.lastUpdated * 1000L, messageType, message.id, distanceFromPrev, deviceName
)
2019-01-25 17:04:26 +01:00
}
2019-02-08 18:12:17 +01:00
fun getMessageType(message: TdApi.Message): Int {
val oldContent = message.content
2019-02-08 18:12:17 +01:00
return when (oldContent) {
is TdApi.MessageText -> LocationMessages.TYPE_TEXT
is TdApi.MessageLocation -> LocationMessages.TYPE_MAP
is MessageLocation -> oldContent.type
else -> -1
}
}
2019-01-25 17:04:26 +01:00
fun formatLocation(sig: Location): String {
return String.format(Locale.US, "%.5f, %.5f", sig.latitude, sig.longitude)
}
2019-01-27 19:59:07 +01:00
fun formatLocation(sig: LocationMessage): String {
2019-01-25 17:04:26 +01:00
return String.format(Locale.US, "%.5f, %.5f", sig.lat, sig.lon)
}
2019-01-28 17:09:35 +01:00
fun formatLocation(sig: BufferMessage): String {
return String.format(Locale.US, "%.5f, %.5f", sig.lat, sig.lon)
}
2019-01-25 17:04:26 +01:00
fun formatFullTime(ti: Long): String {
val dt = Date(ti)
return UTC_DATE_FORMAT.format(dt) + " " + UTC_TIME_FORMAT.format(dt) + " UTC"
}
fun parseOsmAndBotLocationContent(oldContent: MessageOsmAndBotLocation, content: TdApi.MessageContent): MessageOsmAndBotLocation {
val messageLocation = content as TdApi.MessageLocation
return MessageOsmAndBotLocation().apply {
2019-02-08 18:12:17 +01:00
deviceName = oldContent.deviceName
2019-01-25 17:04:26 +01:00
lat = messageLocation.location.latitude
lon = messageLocation.location.longitude
lastUpdated = (System.currentTimeMillis() / 1000).toInt()
2019-02-08 18:12:17 +01:00
type = LocationMessages.TYPE_MAP
2019-01-25 17:04:26 +01:00
}
}
2019-02-08 18:12:17 +01:00
fun parseTextLocation(text: TdApi.FormattedText, botLocation: Boolean): MessageLocation {
val res = if (botLocation) MessageOsmAndBotLocation() else MessageUserLocation()
2019-02-08 18:12:17 +01:00
res.type = LocationMessages.TYPE_TEXT
2019-01-25 17:04:26 +01:00
var locationNA = false
for (s in text.text.lines()) {
when {
s.startsWith(DEVICE_PREFIX) -> {
if (res is MessageOsmAndBotLocation) {
2019-02-08 18:12:17 +01:00
res.deviceName = s.removePrefix(DEVICE_PREFIX)
2019-01-25 17:04:26 +01:00
}
}
s.startsWith(LOCATION_PREFIX) || s.startsWith(LAST_LOCATION_PREFIX) -> {
var locStr: String
var parse = true
if (s.startsWith(LAST_LOCATION_PREFIX)) {
locStr = s.removePrefix(LAST_LOCATION_PREFIX)
if (!locationNA) {
parse = false
}
} else {
locStr = s.removePrefix(LOCATION_PREFIX)
if (locStr.trim() == "n/a") {
locationNA = true
parse = false
}
}
if (parse) {
try {
val urlTextEntity =
text.entities.firstOrNull { it.type is TdApi.TextEntityTypeTextUrl }
if (urlTextEntity != null && urlTextEntity.offset == text.text.indexOf(
locStr
)
) {
val url = (urlTextEntity.type as TdApi.TextEntityTypeTextUrl).url
val point: GeoPointParserUtil.GeoParsedPoint? =
GeoPointParserUtil.parse(url)
if (point != null) {
res.lat = point.latitude
res.lon = point.longitude
}
} else {
val (latS, lonS) = locStr.split(" ")
res.lat = latS.dropLast(1).toDouble()
res.lon = lonS.toDouble()
val timeIndex = locStr.indexOf("(")
if (timeIndex != -1) {
val updatedS = locStr.substring(timeIndex, locStr.length)
res.lastUpdated =
2019-02-08 18:12:17 +01:00
(parseTime(updatedS.removePrefix("(").removeSuffix(")")) / 1000).toInt()
2019-01-25 17:04:26 +01:00
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
s.startsWith(ALTITUDE_PREFIX) -> {
val altStr = s.removePrefix(ALTITUDE_PREFIX)
try {
val alt = altStr.split(" ").first()
res.altitude = alt.toDouble()
} catch (e: Exception) {
e.printStackTrace()
}
}
s.startsWith(SPEED_PREFIX) -> {
val altStr = s.removePrefix(SPEED_PREFIX)
try {
val alt = altStr.split(" ").first()
res.speed = alt.toDouble()
} catch (e: Exception) {
e.printStackTrace()
}
}
s.startsWith(HDOP_PREFIX) -> {
val altStr = s.removePrefix(HDOP_PREFIX)
try {
val alt = altStr.split(" ").first()
res.hdop = alt.toDouble()
} catch (e: Exception) {
e.printStackTrace()
}
}
s.startsWith(UPDATED_PREFIX) -> {
if (res.lastUpdated == 0) {
val updatedStr = s.removePrefix(UPDATED_PREFIX)
val endIndex = updatedStr.indexOf("(")
val updatedS = updatedStr.substring(
0,
if (endIndex != -1) endIndex else updatedStr.length
)
val parsedTime = (parseTime(updatedS.trim()) / 1000).toInt()
val currentTime = (System.currentTimeMillis() / 1000) - 1
res.lastUpdated =
2019-02-08 18:12:17 +01:00
if (parsedTime < currentTime) parsedTime else currentTime.toInt()
2019-01-25 17:04:26 +01:00
}
}
}
}
return res
}
fun parseTime(timeS: String): Long {
try {
when {
timeS.endsWith(FEW_SECONDS_AGO) -> return System.currentTimeMillis() - 5000
timeS.endsWith(SECONDS_AGO_SUFFIX) -> {
val locStr = timeS.removeSuffix(SECONDS_AGO_SUFFIX)
return System.currentTimeMillis() - locStr.toLong() * 1000
}
timeS.endsWith(MINUTES_AGO_SUFFIX) -> {
val locStr = timeS.removeSuffix(MINUTES_AGO_SUFFIX)
val minutes = locStr.toLong()
return System.currentTimeMillis() - minutes * 60 * 1000
}
timeS.endsWith(HOURS_AGO_SUFFIX) -> {
val locStr = timeS.removeSuffix(HOURS_AGO_SUFFIX)
val hours = locStr.toLong()
return (System.currentTimeMillis() - hours * 60 * 60 * 1000)
}
timeS.endsWith(UTC_FORMAT_SUFFIX) -> {
val locStr = timeS.removeSuffix(UTC_FORMAT_SUFFIX)
val (latS, lonS) = locStr.split(" ")
val date = UTC_DATE_FORMAT.parse(latS)
val time = UTC_TIME_FORMAT.parse(lonS)
val res = date.time + time.time
return res
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
2019-01-27 19:59:07 +01:00
fun getTextMessageContent(updateId: Int, location: LocationMessage): TdApi.InputMessageText {
2019-01-28 17:09:35 +01:00
val entities = mutableListOf<TdApi.TextEntity>()
val builder = StringBuilder()
val locationMessage = formatLocation(location)
val firstSpace = USER_TEXT_LOCATION_TITLE.indexOf(' ')
val secondSpace = USER_TEXT_LOCATION_TITLE.indexOf(' ', firstSpace + 1)
entities.add(TdApi.TextEntity(builder.length + firstSpace + 1, secondSpace - firstSpace, TdApi.TextEntityTypeTextUrl(SHARING_LINK)))
builder.append("$USER_TEXT_LOCATION_TITLE\n")
entities.add(TdApi.TextEntity(builder.lastIndex, LOCATION_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(LOCATION_PREFIX)
entities.add(TdApi.TextEntity(builder.length, locationMessage.length,
TdApi.TextEntityTypeTextUrl("$BASE_SHARING_URL?lat=${location.lat}&lon=${location.lon}")))
builder.append("$locationMessage\n")
if (location.altitude != 0.0) {
entities.add(TdApi.TextEntity(builder.lastIndex, ALTITUDE_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(String.format(Locale.US, "$ALTITUDE_PREFIX%.1f m\n", location.altitude))
}
if (location.speed > 0) {
entities.add(TdApi.TextEntity(builder.lastIndex, SPEED_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(String.format(Locale.US, "$SPEED_PREFIX%.1f m/s\n", location.speed))
}
if (location.hdop != 0.0 && location.speed == 0.0) {
entities.add(TdApi.TextEntity(builder.lastIndex, HDOP_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(String.format(Locale.US, "$HDOP_PREFIX%d m\n", location.hdop.toInt()))
}
if (updateId == 0) {
2019-01-29 18:03:44 +01:00
builder.append(String.format("$UPDATED_PREFIX%s\n", formatFullTime(location.time)))
2019-01-28 17:09:35 +01:00
} else {
2019-01-29 18:03:44 +01:00
builder.append(String.format("$UPDATED_PREFIX%s (%d)\n", formatFullTime(location.time), updateId))
2019-01-28 17:09:35 +01:00
}
val textMessage = builder.toString().trim()
return TdApi.InputMessageText(TdApi.FormattedText(textMessage, entities.toTypedArray()), true, true)
}
fun getTextMessageContent(updateId: Int, location: BufferMessage): TdApi.InputMessageText {
2019-01-25 17:04:26 +01:00
val entities = mutableListOf<TdApi.TextEntity>()
val builder = StringBuilder()
val locationMessage = formatLocation(location)
val firstSpace = USER_TEXT_LOCATION_TITLE.indexOf(' ')
val secondSpace = USER_TEXT_LOCATION_TITLE.indexOf(' ', firstSpace + 1)
entities.add(TdApi.TextEntity(builder.length + firstSpace + 1, secondSpace - firstSpace, TdApi.TextEntityTypeTextUrl(SHARING_LINK)))
builder.append("$USER_TEXT_LOCATION_TITLE\n")
entities.add(TdApi.TextEntity(builder.lastIndex, LOCATION_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(LOCATION_PREFIX)
entities.add(TdApi.TextEntity(builder.length, locationMessage.length,
TdApi.TextEntityTypeTextUrl("$BASE_SHARING_URL?lat=${location.lat}&lon=${location.lon}")))
builder.append("$locationMessage\n")
if (location.altitude != 0.0) {
entities.add(TdApi.TextEntity(builder.lastIndex, ALTITUDE_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(String.format(Locale.US, "$ALTITUDE_PREFIX%.1f m\n", location.altitude))
}
if (location.speed > 0) {
entities.add(TdApi.TextEntity(builder.lastIndex, SPEED_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(String.format(Locale.US, "$SPEED_PREFIX%.1f m/s\n", location.speed))
}
if (location.hdop != 0.0 && location.speed == 0.0) {
entities.add(TdApi.TextEntity(builder.lastIndex, HDOP_PREFIX.length, TdApi.TextEntityTypeBold()))
builder.append(String.format(Locale.US, "$HDOP_PREFIX%d m\n", location.hdop.toInt()))
}
if (updateId == 0) {
2019-01-29 18:03:44 +01:00
builder.append(String.format("$UPDATED_PREFIX%s\n", formatFullTime(location.time)))
2019-01-25 17:04:26 +01:00
} else {
2019-01-29 18:03:44 +01:00
builder.append(String.format("$UPDATED_PREFIX%s (%d)\n", formatFullTime(location.time), updateId))
2019-01-25 17:04:26 +01:00
}
val textMessage = builder.toString().trim()
return TdApi.InputMessageText(TdApi.FormattedText(textMessage, entities.toTypedArray()), true, true)
}
2019-01-27 19:59:07 +01:00
fun convertLocationMessagesToGpxFiles(items: List<LocationMessage>, newGpxPerChat: Boolean = true): List<GPXUtilities.GPXFile> {
2019-01-25 17:04:26 +01:00
val dataTracks = ArrayList<GPXUtilities.GPXFile>()
var previousTime: Long = -1
var previousChatId: Long = -1
var previousUserId = -1
var segment: GPXUtilities.TrkSegment? = null
var track: GPXUtilities.Track? = null
var gpx: GPXUtilities.GPXFile? = null
2019-02-01 12:34:22 +01:00
var countedLocations = 0
2019-01-25 17:04:26 +01:00
items.forEach {
val userId = it.userId
val chatId = it.chatId
2019-01-29 18:03:44 +01:00
val time = it.time
2019-02-01 12:34:22 +01:00
if (previousTime >= time) {
return@forEach
}
countedLocations++
2019-01-25 17:04:26 +01:00
if (previousUserId != userId || (newGpxPerChat && previousChatId != chatId)) {
gpx = GPXUtilities.GPXFile()
gpx!!.chatId = chatId
gpx!!.userId = userId
previousTime = 0
track = null
segment = null
dataTracks.add(gpx!!)
}
val pt = GPXUtilities.WptPt()
pt.userId = userId
pt.chatId = chatId
pt.lat = it.lat
pt.lon = it.lon
pt.ele = it.altitude
pt.speed = it.speed
pt.hdop = it.hdop
pt.time = time
val currentInterval = Math.abs(time - previousTime)
if (track != null) {
if (currentInterval < 30 * 60 * 1000) {
// 30 minute - same segment
segment!!.points.add(pt)
} else {
segment = GPXUtilities.TrkSegment()
segment!!.points.add(pt)
track!!.segments.add(segment)
}
} else {
track = GPXUtilities.Track()
segment = GPXUtilities.TrkSegment()
track!!.segments.add(segment)
segment!!.points.add(pt)
gpx!!.tracks.add(track)
}
previousTime = time
previousUserId = userId
previousChatId = chatId
}
return dataTracks
}
fun saveGpx(app: TelegramApplication, listener: SaveGpxListener, dir: File, gpxFile: GPXUtilities.GPXFile) {
if (!gpxFile.isEmpty) {
val task = SaveGPXTrackToFileTask(app, listener, gpxFile, dir, 0)
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
}
}
abstract class MessageLocation : TdApi.MessageContent() {
var lat: Double = Double.NaN
internal set
var lon: Double = Double.NaN
internal set
var lastUpdated: Int = 0
internal set
var speed: Double = 0.0
internal set
var altitude: Double = 0.0
internal set
var hdop: Double = 0.0
internal set
var bearing: Double = 0.0
internal set
2019-01-29 18:03:44 +01:00
var type: Int = -1
internal set
2019-01-25 17:04:26 +01:00
override fun getConstructor() = -1
abstract fun isValid(): Boolean
}
class MessageOsmAndBotLocation : MessageLocation() {
2019-02-08 18:12:17 +01:00
var deviceName: String = ""
2019-01-25 17:04:26 +01:00
internal set
2019-02-08 18:12:17 +01:00
override fun isValid() = deviceName != "" && lat != Double.NaN && lon != Double.NaN
2019-01-25 17:04:26 +01:00
}
class MessageUserLocation : MessageLocation() {
2019-01-25 17:04:26 +01:00
override fun isValid() = lat != Double.NaN && lon != Double.NaN
}
private class SaveGPXTrackToFileTask internal constructor(
private val app: TelegramApplication, private val listener: SaveGpxListener?,
private val gpxFile: GPXUtilities.GPXFile, private val dir: File, private val userId: Int
) :
AsyncTask<Void, Void, List<String>>() {
override fun doInBackground(vararg params: Void): List<String> {
val warnings = ArrayList<String>()
dir.mkdirs()
if (dir.parentFile.canWrite()) {
if (dir.exists()) {
// save file
var fout = File(dir, "$userId.gpx")
if (!gpxFile.isEmpty) {
val pt = gpxFile.findPointToShow()
val user = app.telegramHelper.getUser(pt!!.userId)
val fileName: String
fileName = if (user != null) {
2019-02-01 16:13:21 +01:00
(TelegramUiHelper.getUserName(user) + "_" + SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date(pt.time)))
2019-01-25 17:04:26 +01:00
} else {
2019-02-01 16:13:21 +01:00
userId.toString() + "_" + SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date(pt.time))
2019-01-25 17:04:26 +01:00
}
fout = File(dir, "$fileName.gpx")
}
val warn = GPXUtilities.writeGpxFile(fout, gpxFile, app)
if (warn != null) {
warnings.add(warn)
return warnings
}
}
}
return warnings
}
override fun onPostExecute(warnings: List<String>?) {
if (listener != null) {
if (warnings != null && warnings.isEmpty()) {
listener.onSavingGpxFinish(gpxFile.path)
} else {
listener.onSavingGpxError(warnings)
}
}
}
}
interface SaveGpxListener {
fun onSavingGpxFinish(path: String)
fun onSavingGpxError(warnings: List<String>?)
}
}