This commit is contained in:
qiji4215 2023-04-25 15:47:37 +08:00
commit f96292b686
28 changed files with 888 additions and 191 deletions

View File

@ -35,7 +35,7 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
buildFeatures {
viewBinding true
@ -91,7 +91,6 @@ dependencies {
// spatialite文件
implementation 'com.github.sevar83:android-spatialite:2.0.1'
}
//
kapt {

View File

@ -16,7 +16,6 @@ class OMQSApplication : Application() {
Util.getInstance().init(applicationContext)
NetUtils.getInstance().init(this)
TakePhotoManager.getInstance().init(this, 1)
FileManager.initRootDir(this)
}
private fun getKey(inputString: String): String {

View File

@ -5,6 +5,7 @@ import com.navinfo.omqs.Constant
import com.navinfo.omqs.tools.FileManager.Companion.FileDownloadStatus
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
@ -55,9 +56,11 @@ open class TaskBean @JvmOverloads constructor(
/**
* 当前下载状态
*/
var status: Int = FileDownloadStatus.NONE
) : RealmObject(){
fun getDownLoadUrl():String{
var status: Int = FileDownloadStatus.NONE,
@Ignore
var message: String = ""
) : RealmObject() {
fun getDownLoadUrl(): String {
return "${Constant.SERVER_ADDRESS}devcp/download?fileStr=26"
}
}

View File

@ -30,13 +30,24 @@ import kotlin.streams.toList
/**
* 导入omdb数据的帮助类
* */
class ImportOMDBHelper @AssistedInject constructor(@Assisted("context") val context: Context,@Assisted("omdbFile") val omdbFile: File) {
class ImportOMDBHelper @AssistedInject constructor(
@Assisted("context") val context: Context,
@Assisted("omdbFile") val omdbFile: File
) {
@Inject
lateinit var omdbHiltFactory: OMDBDataBaseHiltFactory
@Inject
lateinit var gson: Gson
private val database by lazy { omdbHiltFactory.obtainOmdbDataBaseHelper(context, omdbFile.absolutePath, 1).writableDatabase }
private val configFile: File = File("${Constant.USER_DATA_PATH}", Constant.OMDB_CONFIG)
private val database by lazy {
omdbHiltFactory.obtainOmdbDataBaseHelper(
context,
omdbFile.absolutePath,
1
).writableDatabase
}
private val configFile: File =
File("${Constant.DATA_PATH}/${Constant.CURRENT_USER_ID}", Constant.OMDB_CONFIG)
/**
* 读取config的配置文件
@ -67,8 +78,10 @@ class ImportOMDBHelper @AssistedInject constructor(@Assisted("context") val cont
}
}.toList()
val cursor = database.query(table, finalColumns.toTypedArray(), "1=1",
mutableListOf<String>().toTypedArray(), null, null, null, null)
val cursor = database.query(
table, finalColumns.toTypedArray(), "1=1",
mutableListOf<String>().toTypedArray(), null, null, null, null
)
with(cursor) {
if (moveToFirst()) {
while (moveToNext()) {
@ -76,13 +89,17 @@ class ImportOMDBHelper @AssistedInject constructor(@Assisted("context") val cont
for (columnIndex in 0 until columnCount) {
var columnName = getColumnName(columnIndex)
if (columnName.startsWith("ST_AsText(")) {
columnName = columnName.replace("ST_AsText(", "").substringBeforeLast(")")
columnName = columnName.replace("ST_AsText(", "")
.substringBeforeLast(")")
}
when(getType(columnIndex)) {
when (getType(columnIndex)) {
FIELD_TYPE_NULL -> rowMap[columnName] = ""
FIELD_TYPE_INTEGER -> rowMap[columnName] = getInt(columnIndex)
FIELD_TYPE_FLOAT -> rowMap[columnName] = getFloat(columnIndex)
FIELD_TYPE_BLOB -> rowMap[columnName] = String(getBlob(columnIndex), Charsets.UTF_8)
FIELD_TYPE_INTEGER -> rowMap[columnName] =
getInt(columnIndex)
FIELD_TYPE_FLOAT -> rowMap[columnName] =
getFloat(columnIndex)
FIELD_TYPE_BLOB -> rowMap[columnName] =
String(getBlob(columnIndex), Charsets.UTF_8)
else -> rowMap[columnName] = getString(columnIndex)
}
}
@ -104,54 +121,60 @@ class ImportOMDBHelper @AssistedInject constructor(@Assisted("context") val cont
suspend fun importOmdbZipFile(omdbZipFile: File): Flow<String> = withContext(Dispatchers.IO) {
val importConfig = openConfigFile()
val unZipFolder = File(omdbZipFile.parentFile, "result")
flow<String> {
flow {
if (unZipFolder.exists()) {
unZipFolder.deleteRecursively()
}
unZipFolder.mkdirs()
// 开始解压zip文件
val unZipFiles = ZipUtils.unzipFile(omdbZipFile, unZipFolder)
// 将listResult数据插入到Realm数据库中
Realm.getDefaultInstance().beginTransaction()
// 遍历解压后的文件,读取该数据返回
for ((index, currentConfig) in importConfig.tables.withIndex()) {
val txtFile = unZipFiles.find {
it.name == currentConfig.table
}
val listResult: MutableList<Map<String, Any>> = mutableListOf()
val listResult = mutableListOf<Map<String, Any?>>()
currentConfig?.let {
val list = FileIOUtils.readFile2List(txtFile, "UTF-8")
if (list!=null) {
if (list != null) {
// 将list数据转换为map
for (line in list) {
val map = gson.fromJson<Map<String, Any>>(line, object : TypeToken<MutableMap<String, Any>>() {}.type)
val map = gson.fromJson<Map<String, Any?>>(line, object:TypeToken<Map<String, Any?>>(){}.getType())
.toMutableMap()
map["QItable"] = currentConfig.table
map["QIname"] = currentConfig.name
map["QIcode"] = currentConfig.code
map["qi_table"] = currentConfig.table
map["qi_name"] = currentConfig.name
map["qi_code"] = currentConfig.code
listResult.add(map)
}
}
}
// 将listResult数据插入到Realm数据库中
Realm.getDefaultInstance().beginTransaction()
for (map in listResult) { // 每一个map就是Realm的一条数据
// 先查询这个mesh下有没有数据如果有则跳过即可
// val meshEntity = Realm.getDefaultInstance().where(RenderEntity::class.java).equalTo("properties['mesh']", map["mesh"].toString()).findFirst()
val renderEntity = RenderEntity()
renderEntity.code = map["QIcode"].toString().toInt()
renderEntity.name = map["QIname"].toString()
renderEntity.table = map["QItable"].toString()
renderEntity.code = map["qi_code"].toString().toInt()
renderEntity.name = map["qi_name"].toString()
renderEntity.table = map["qi_table"].toString()
// 其他数据插入到Properties中
renderEntity.geometry = map["geometry"].toString()
for (entry in map) {
renderEntity.properties[entry.key] = entry.value.toString()
for ((key, value) in map) {
when (value) {
is String -> renderEntity.properties[key.toString()] = value
is Int -> renderEntity.properties[key.toString()] = value.toInt().toString()
is Double -> renderEntity.properties[key.toString()] = value.toDouble().toString()
else -> renderEntity.properties[key.toString()] = value.toString()
}
}
Realm.getDefaultInstance().insert(renderEntity)
}
Realm.getDefaultInstance().commitTransaction()
// 1个文件发送一次flow流
emit("${index+1}/${importConfig.tables.size}")
emit("${index + 1}/${importConfig.tables.size}")
}
Realm.getDefaultInstance().commitTransaction()
emit("OK")
}
}
@ -160,7 +183,15 @@ class ImportOMDBHelper @AssistedInject constructor(@Assisted("context") val cont
val columns = mutableListOf<String>()
// 查询 sqlite_master 表获取指定数据表的元数据信息
val cursor = db.query("sqlite_master", arrayOf("sql"), "type='table' AND name=?", arrayOf(tableName), null, null, null)
val cursor = db.query(
"sqlite_master",
arrayOf("sql"),
"type='table' AND name=?",
arrayOf(tableName),
null,
null,
null
)
// 从元数据信息中解析出列名
if (cursor.moveToFirst()) {

View File

@ -1,9 +1,9 @@
package com.navinfo.omqs.db
import com.navinfo.collect.library.data.entity.QsRecordBean
import com.navinfo.omqs.bean.HadLinkDvoBean
import com.navinfo.omqs.bean.TaskBean
import io.realm.annotations.RealmModule
@io.realm.annotations.RealmModule(classes = [TaskBean::class, HadLinkDvoBean::class])
@RealmModule(classes = [TaskBean::class, HadLinkDvoBean::class])
class MyRealmModule {
}

View File

@ -0,0 +1,66 @@
package com.navinfo.omqs.hilt
import android.content.Context
import com.navinfo.collect.library.map.NIMapController
import com.navinfo.omqs.db.RealmOperateHelper
import com.navinfo.omqs.db.RoomAppDatabase
import com.navinfo.omqs.http.RetrofitNetworkServiceAPI
import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager
import com.navinfo.omqs.http.taskdownload.TaskDownloadManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
@InstallIn(ActivityRetainedComponent::class)
@Module
class ActivityModule {
/**
* 注入地图控制器在activity范围内使用单例
*/
@ActivityRetainedScoped
@Provides
fun providesMapController(): NIMapController = NIMapController()
/**
* 注入离线地图下载管理在activity范围内使用单例
*/
@ActivityRetainedScoped
@Provides
fun providesOfflineMapDownloadManager(
networkServiceAPI: RetrofitNetworkServiceAPI,
roomAppDatabase: RoomAppDatabase,
mapController: NIMapController
): OfflineMapDownloadManager =
OfflineMapDownloadManager(networkServiceAPI, roomAppDatabase, mapController)
/**
* 注入任务下载
*/
@ActivityRetainedScoped
@Provides
fun providesTaskListDownloadManager(
networkServiceAPI: RetrofitNetworkServiceAPI,
importFactory: ImportOMDBHiltFactory,
): TaskDownloadManager =
TaskDownloadManager(importFactory, networkServiceAPI)
/**
* 实验失败这样创建viewmodel不会在activity销毁的时候同时销毁
* 4-14:因为没有传入activity的 owner,无法检测生命周期
*/
// @ActivityRetainedScoped
// @Provides
// fun providesMainViewModel(mapController: NIMapController): MainViewModel {
// return MainViewModel(mapController)
// }
@ActivityRetainedScoped
@Provides
fun providesRealmOperateHelper(): RealmOperateHelper {
return RealmOperateHelper()
}
}

View File

@ -1,20 +1,23 @@
package com.navinfo.omqs.hilt
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.room.Room
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.navinfo.omqs.Constant
import com.navinfo.omqs.OMQSApplication
import com.navinfo.omqs.db.RoomAppDatabase
import com.navinfo.omqs.http.RetrofitNetworkServiceAPI
import com.navinfo.omqs.tools.IntTypeAdapter
import com.tencent.wcdb.database.SQLiteCipherSpec
import com.tencent.wcdb.room.db.WCDBOpenHelperFactory
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.realm.Realm
import kotlinx.coroutines.*
@ -34,7 +37,7 @@ class GlobalModule {
@Singleton
@Provides
fun provideApplication(application: Application): OMQSApplication {
fun provideApplication(@ApplicationContext application: Application): OMQSApplication {
return application as OMQSApplication
}
@ -86,7 +89,12 @@ class GlobalModule {
@Provides
@Singleton
fun provideGson(): Gson = Gson()
fun provideGson(): Gson = GsonBuilder()
// 解决解析Json时将int类型自动转换为Double的问题
.registerTypeAdapter(object : TypeToken<Map<String, Any?>>() {}.getType(), IntTypeAdapter())
.registerTypeAdapter(object : TypeToken<Map<String, Any>>() {}.getType(), IntTypeAdapter())
.registerTypeAdapter(object : TypeToken<Map<Any, Any>>() {}.getType(), IntTypeAdapter())
.create()
@Provides
@Singleton

View File

@ -1,63 +1,21 @@
package com.navinfo.omqs.hilt
import android.content.Context
import com.navinfo.collect.library.map.NIMapController
import com.navinfo.omqs.db.RealmOperateHelper
import com.navinfo.omqs.db.RoomAppDatabase
import com.navinfo.omqs.http.RetrofitNetworkServiceAPI
import com.navinfo.omqs.http.offlinemapdownload.OfflineMapDownloadManager
import com.navinfo.omqs.http.taskdownload.TaskDownloadManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.scopes.ActivityRetainedScoped
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
@InstallIn(ActivityRetainedComponent::class)
@InstallIn(ActivityComponent::class)
@Module
class MainActivityModule {
/**
* 注入地图控制器在activity范围内使用单例
*/
@ActivityRetainedScoped
@ActivityScoped
@Provides
fun providesMapController(): NIMapController = NIMapController()
/**
* 注入离线地图下载管理在activity范围内使用单例
*/
@ActivityRetainedScoped
@Provides
fun providesOfflineMapDownloadManager(
networkServiceAPI: RetrofitNetworkServiceAPI,
roomAppDatabase: RoomAppDatabase,
mapController: NIMapController
): OfflineMapDownloadManager =
OfflineMapDownloadManager(networkServiceAPI, roomAppDatabase, mapController)
/**
* 注入任务下载
*/
@ActivityRetainedScoped
@Provides
fun providesTaskListDownloadManager(
networkServiceAPI: RetrofitNetworkServiceAPI,
): TaskDownloadManager =
TaskDownloadManager(networkServiceAPI)
/**
* 实验失败这样创建viewmodel不会在activity销毁的时候同时销毁
* 4-14:因为没有传入activity的 owner,无法检测生命周期
*/
// @ActivityRetainedScoped
// @Provides
// fun providesMainViewModel(mapController: NIMapController): MainViewModel {
// return MainViewModel(mapController)
// }
@ActivityRetainedScoped
@Provides
fun providesRealmOperateHelper(): RealmOperateHelper {
return RealmOperateHelper()
}
fun providesContext(@ActivityContext context: Context): Context = context
}

View File

@ -0,0 +1,7 @@
//package com.navinfo.omqs.hilt
//
//import javax.inject.Qualifier
//
//@Qualifier
//@Retention(AnnotationRetention.RUNTIME)
//annotation class ActivityContext

View File

@ -1,23 +1,32 @@
package com.navinfo.omqs.http.taskdownload
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.navinfo.omqs.bean.TaskBean
import com.navinfo.omqs.hilt.ImportOMDBHiltFactory
import com.navinfo.omqs.hilt.OMDBDataBaseHiltFactory
import com.navinfo.omqs.http.RetrofitNetworkServiceAPI
import dagger.hilt.android.qualifiers.ActivityContext
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
/**
* 管理任务数据下载
*/
class TaskDownloadManager(
class TaskDownloadManager constructor(
val importFactory: ImportOMDBHiltFactory,
val netApi: RetrofitNetworkServiceAPI,
) {
lateinit var context: Context
/**
* 最多同时下载数量
*/
private val MAX_SCOPE = 3
private val MAX_SCOPE = 1
/**
* 存储有哪些城市需要下载的队列
@ -33,6 +42,9 @@ class TaskDownloadManager(
ConcurrentHashMap<Int, TaskDownloadScope>()
}
fun init(context: Context) {
this.context = context
}
/**
* 启动下载任务
@ -92,7 +104,7 @@ class TaskDownloadManager(
fun addTask(taskBean: TaskBean) {
if (!scopeMap.containsKey(taskBean.id)) {
scopeMap[taskBean.id] = TaskDownloadScope(this, taskBean)
scopeMap[taskBean.id] = TaskDownloadScope( this, taskBean)
}
}

View File

@ -1,23 +1,34 @@
package com.navinfo.omqs.http.taskdownload
import android.content.Context
import android.util.Log
import androidx.core.content.ContentProviderCompat.requireContext
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.navinfo.omqs.Constant
import com.navinfo.omqs.bean.TaskBean
import com.navinfo.omqs.db.ImportOMDBHelper
import com.navinfo.omqs.hilt.ImportOMDBHiltFactory
import com.navinfo.omqs.tools.FileManager
import com.navinfo.omqs.tools.FileManager.Companion.FileDownloadStatus
import io.realm.Realm
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.RandomAccessFile
import javax.inject.Inject
class TaskDownloadScope(
private val downloadManager: TaskDownloadManager,
val taskBean: TaskBean,
) :
CoroutineScope by CoroutineScope(Dispatchers.IO + CoroutineName("OfflineMapDownLoad")) {
@Inject
lateinit var importOMDBHiltFactory: ImportOMDBHiltFactory
/**
*下载任务用来取消的
*/
@ -59,7 +70,12 @@ class TaskDownloadScope(
*/
fun launch() {
downloadJob = launch() {
download()
FileManager.checkOMDBFileInfo(taskBean)
if (taskBean.status == FileDownloadStatus.IMPORT) {
importData()
} else {
download()
}
downloadManager.launchNext(taskBean.id)
}
}
@ -76,14 +92,17 @@ class TaskDownloadScope(
* 更新任务
* @param status [OfflineMapCityBean.Status]
*/
private fun change(status: Int) {
if (taskBean.status != status || status == FileDownloadStatus.LOADING) {
private fun change(status: Int, message: String = "") {
if (taskBean.status != status || status == FileDownloadStatus.LOADING || status == FileDownloadStatus.IMPORTING) {
taskBean.status = status
taskBean.message = message
downloadData.postValue(taskBean)
launch(Dispatchers.IO) {
// downloadManager.roomDatabase.getOfflineMapDao().update(taskBean)
launch {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
it.copyToRealmOrUpdate(taskBean)
}
}
}
}
@ -96,36 +115,75 @@ class TaskDownloadScope(
downloadData.observe(owner, ob)
}
/**
* 导入数据
*/
private suspend fun importData(file: File? = null) {
try {
Log.e("jingo", "importData SSS")
change(FileDownloadStatus.IMPORTING)
var fileNew = file
?: File("${Constant.DOWNLOAD_PATH}${taskBean.evaluationTaskName}_${taskBean.dataVersion}.zip")
val importOMDBHelper: ImportOMDBHelper =
downloadManager.importFactory.obtainImportOMDBHelper(
downloadManager.context,
fileNew
)
importOMDBHelper.importOmdbZipFile(importOMDBHelper.omdbFile).collect {
Log.e("jingo", "数据安装 $it")
if (it == "OK") {
change(FileDownloadStatus.DONE)
} else {
change(FileDownloadStatus.IMPORTING, it)
}
}
} catch (e: Exception) {
Log.e("jingo", "数据安装失败 ${e.toString()}")
change(FileDownloadStatus.ERROR)
}
Log.e("jingo", "importData EEE")
}
/**
* 下载文件
*/
private suspend fun download() {
//如果文件下载安装已经完毕
if (taskBean.status == FileDownloadStatus.DONE) {
return
}
var inputStream: InputStream? = null
var randomAccessFile: RandomAccessFile? = null
try {
//创建离线地图 下载文件夹,.map文件夹的下一级
val fileDir = File("${Constant.DOWNLOAD_PATH}download")
val fileDir = File("${Constant.DOWNLOAD_PATH}")
if (!fileDir.exists()) {
fileDir.mkdirs()
}
val fileTemp =
File("${Constant.DOWNLOAD_PATH}${taskBean.id}_${taskBean.dataVersion}")
File("${Constant.DOWNLOAD_PATH}${taskBean.evaluationTaskName}_${taskBean.dataVersion}.zip")
val startPosition = taskBean.currentSize
//验证断点有效性
if (startPosition < 0) throw IOException("jingo Start position less than zero")
if (fileTemp.length() > 0 && taskBean.fileSize > 0 && fileTemp.length() == taskBean.fileSize) {
importData(fileTemp)
return
}
val response = downloadManager.netApi.retrofitDownLoadFile(
start = "bytes=$startPosition-",
url = taskBean.getDownLoadUrl()
)
val responseBody = response.body()
change(FileDownloadStatus.LOADING)
responseBody ?: throw IOException("jingo ResponseBody is null")
responseBody ?: throw IOException("jingo ResponseBody is null")
if (startPosition == 0L) {
taskBean.fileSize = responseBody.contentLength()
Log.e("jingo", "当前文件大小 ${taskBean.fileSize}")
}
change(FileDownloadStatus.LOADING)
//写入文件
randomAccessFile = RandomAccessFile(fileTemp, "rwd")
randomAccessFile.seek(startPosition)
@ -146,17 +204,14 @@ class TaskDownloadScope(
}
}
Log.e("jingo", "文件下载完成 ${taskBean.currentSize} == ${taskBean.fileSize}")
if (taskBean.currentSize == taskBean.fileSize) {
val res =
fileTemp.renameTo(File("${Constant.DOWNLOAD_PATH}${taskBean.evaluationTaskName}.zip"))
Log.e("jingo", "文件下载完成 修改文件 $res")
change(FileDownloadStatus.DONE)
importData(fileTemp)
} else {
change(FileDownloadStatus.PAUSE)
}
} catch (e: Throwable) {
change(FileDownloadStatus.ERROR)
Log.e("jingo","数据下载出错 ${e.message}")
} finally {
inputStream?.close()
randomAccessFile?.close()

View File

@ -16,8 +16,10 @@ class FileManager {
const val LOADING = 2 //下载中
const val PAUSE = 3 //暂停
const val ERROR = 4 //错误
const val DONE = 5 //完成
const val UPDATE = 6 //有新版本要更新
const val IMPORT = 5 //安装
const val IMPORTING = 6 //安装中
const val UPDATE = 7 //有新版本要更新
const val DONE = 8 //完成
}
//初始化数据文件夹
@ -109,60 +111,28 @@ class FileManager {
* 检查离线地图文件
*/
suspend fun checkOMDBFileInfo(taskBean: TaskBean) {
if (taskBean.status == FileDownloadStatus.DONE)
return
//访问离线地图文件夹
val fileDir = File("${Constant.DOWNLOAD_PATH}")
//如果连本地文件夹还没有,就不用修改任何数据了
if (!fileDir.exists()) {
return
}
//访问离线地图临时下载文件夹
val fileTempDir = File(Constant.DOWNLOAD_PATH)
//是否有一份.map文件了
var mapFile: File? = null
//文件夹里文件挨个访问
for (item in fileDir.listFiles()) {
//先找到对应的省市文件例如540000_西藏自治区_20230401195018.map",以id开头
if (item.isFile && item.name.startsWith("${taskBean.id}_")) {
//如果本地文件与从网络获取到版本号一致,表示这个文件已经下载完毕,不用处理了
if (item.name == "${taskBean.id}_${taskBean.dataVersion}") {
taskBean.status = FileDownloadStatus.DONE
return
}
//文件存在,版本号不对应,留给下面流程处理
mapFile = item
break
}
}
//临时下载文件夹
if (fileTempDir.exists()) {
for (item in fileTempDir.listFiles()) {
//先找到对应的省市文件例如540000_20230401195018",以id开头
if (item.isFile && item.name.startsWith("${taskBean.id}_")) {
//如果本地文件与从网络获取到版本号一致,表示这个文件已经在下载列表中
if (item.name == "${taskBean.id}_${taskBean.dataVersion}") {
//如果这个临时文件的大小和下载大小是一致的,说明已经下载完了,但是在下载环节没有更名移动成功,需要重命名和移动文件夹
if (item.length() == taskBean.fileSize) {
//移动更名文件后删除旧数据,修改状态
if (item.renameTo(File("${Constant.OFFLINE_MAP_PATH}${taskBean.evaluationTaskName}.zip"))) {
//删除旧版本数据
mapFile?.delete()
taskBean.status = FileDownloadStatus.DONE
return
}
} else { // 临时文件大小和目标不一致,说明下载了一半
taskBean.status = FileDownloadStatus.PAUSE
taskBean.currentSize = item.length()
return
}
} else { //虽然省市id开头一致但是版本号不一致说明之前版本下载了一部分现在要更新了原来下载的文件直接删除
taskBean.status = FileDownloadStatus.UPDATE
item.delete()
return
}
break
if (item.isFile && item.name == "${taskBean.evaluationTaskName}_${taskBean.dataVersion}.zip") {
taskBean.currentSize = item.length()
if (taskBean.fileSize > 0 && taskBean.fileSize == item.length()) {
taskBean.status = FileDownloadStatus.IMPORT
} else {
taskBean.status = FileDownloadStatus.PAUSE
}
return
}
}
taskBean.status = FileDownloadStatus.NONE
}
}
}

View File

@ -0,0 +1,69 @@
package com.navinfo.omqs.tools
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.internal.LinkedTreeMap
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import java.io.IOException
class IntTypeAdapter : TypeAdapter<Any>() {
private val delegate: TypeAdapter<Any> = Gson().getAdapter(Any::class.java)
@Throws(IOException::class)
override fun read(`in`: JsonReader): Any? {
val token = `in`.peek()
when (token) {
JsonToken.BEGIN_ARRAY -> {
val list: MutableList<Any?> = ArrayList()
`in`.beginArray()
while (`in`.hasNext()) {
list.add(read(`in`))
}
`in`.endArray()
return list
}
JsonToken.BEGIN_OBJECT -> {
val map: MutableMap<String, Any?> = LinkedTreeMap()
`in`.beginObject()
while (`in`.hasNext()) {
map[`in`.nextName()] = read(`in`)
}
`in`.endObject()
return map
}
JsonToken.STRING -> return `in`.nextString()
JsonToken.NUMBER -> {
// 改写数字的处理逻辑,将数字值分为整型与浮点型。
val dbNum = `in`.nextDouble()
// 数字超过long的最大值返回浮点类型
if (dbNum > Long.MAX_VALUE) {
return dbNum
}
// 判断数字是否为整数值
val lngNum = dbNum.toLong()
return if (dbNum == lngNum.toDouble()) {
try {
lngNum.toInt()
} catch (e: Exception) {
lngNum
}
} else {
dbNum
}
}
JsonToken.BOOLEAN -> return `in`.nextBoolean()
JsonToken.NULL -> {
`in`.nextNull()
return null
}
else -> throw IllegalStateException()
}
}
@Throws(IOException::class)
override fun write(out: JsonWriter, value: Any?) {
delegate.write(out, value)
}
}

View File

@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ResourceUtils
import com.navinfo.omqs.Constant
import com.navinfo.omqs.bean.LoginUserBean
import com.navinfo.omqs.db.MyRealmModule
import com.navinfo.omqs.db.RoomAppDatabase
import com.navinfo.omqs.http.NetResult
import com.navinfo.omqs.http.NetworkService
@ -159,7 +160,8 @@ class LoginViewModel @Inject constructor(
Constant.VERSION_ID = userId
Constant.USER_DATA_PATH = Constant.DATA_PATH + Constant.USER_ID + "/" + Constant.VERSION_ID
// 在SD卡创建用户目录解压资源等
val userFolder = File("${Constant.DATA_PATH}/${userId}")
val userFolder = File(Constant.USER_DATA_PATH)
if (!userFolder.exists()) userFolder.mkdirs()
// 初始化Realm
Realm.init(context.applicationContext)
val password = "encryp".encodeToByteArray().copyInto(ByteArray(64))
@ -169,7 +171,7 @@ class LoginViewModel @Inject constructor(
.directory(userFolder)
.name("OMQS.realm")
.encryptionKey(password)
// .modules(Realm.getDefaultModule(), MyRealmModule())
.modules(Realm.getDefaultModule(), MyRealmModule())
.schemaVersion(1)
.build()
Realm.setDefaultConfiguration(config)

View File

@ -4,10 +4,8 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.blankj.utilcode.util.ToastUtils
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import com.blankj.utilcode.util.ToastUtils
import com.navinfo.collect.library.map.NIMapController
import com.navinfo.collect.library.map.handler.NiLocationListener
import com.navinfo.omqs.Constant
@ -34,10 +32,10 @@ class MainActivity : BaseActivity() {
@Inject
lateinit var offlineMapDownloadManager: OfflineMapDownloadManager
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
//初始化地图
mapController.init(

View File

@ -3,38 +3,24 @@ package com.navinfo.omqs.ui.fragment.personalcenter
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.blankj.utilcode.util.UriUtils
import com.github.k1rakishou.fsaf.FileChooser
import com.github.k1rakishou.fsaf.callback.FSAFActivityCallbacks
import com.github.k1rakishou.fsaf.callback.FileChooserCallback
import com.navinfo.collect.library.data.RealmUtils
import com.navinfo.collect.library.data.entity.OMDBEntity
import com.navinfo.collect.library.map.NIMapController
import com.navinfo.omqs.R
import com.navinfo.omqs.databinding.FragmentPersonalCenterBinding
import com.navinfo.omqs.db.ImportOMDBHelper
import com.navinfo.omqs.hilt.ImportOMDBHiltFactory
import com.navinfo.omqs.tools.CoroutineUtils
import com.navinfo.omqs.ui.activity.BaseActivity
import com.navinfo.omqs.ui.fragment.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
import io.realm.Realm
import io.realm.RealmDictionary
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.oscim.core.GeoPoint
import java.io.File
import java.util.UUID
import javax.inject.Inject
/**
@ -104,13 +90,6 @@ class PersonalCenterFragment : BaseFragment(), FSAFActivityCallbacks {
file
)
viewModel.importOMDBData(importOMDBHelper)
// // 开始导入数据
// CoroutineUtils.launchWithLoading(
// requireContext(),
// loadingMessage = "导入数据..."
// ) {
//
// }
}
})
}
@ -122,14 +101,13 @@ class PersonalCenterFragment : BaseFragment(), FSAFActivityCallbacks {
override fun onResult(uri: Uri) {
viewModel.importScProblemData(uri)
}
})
}
R.id.personal_center_menu_test -> {
viewModel.readRealmData()
// 定位到指定位置
niMapController.mMapView.vtmMap.animator().animateTo(GeoPoint(28.608398, 115.67901))
niMapController.mMapView.vtmMap.animator().animateTo(GeoPoint(30.270367985798032, 113.83513667119433))
}
R.id.personal_center_menu_task_list -> {
findNavController().navigate(R.id.TaskListFragment)

View File

@ -13,6 +13,8 @@ import com.navinfo.omqs.http.taskdownload.TaskDownloadManager
import com.navinfo.omqs.tools.FileManager.Companion.FileDownloadStatus
import com.navinfo.omqs.ui.other.BaseRecyclerViewAdapter
import com.navinfo.omqs.ui.other.BaseViewHolder
import java.io.File
import javax.inject.Inject
/**
* 离线地图城市列表 RecyclerView 适配器
@ -31,7 +33,7 @@ class TaskListAdapter(
if (it.tag != null) {
val taskBean = data[it.tag as Int]
when (taskBean.status) {
FileDownloadStatus.NONE, FileDownloadStatus.UPDATE, FileDownloadStatus.PAUSE, FileDownloadStatus.ERROR -> {
FileDownloadStatus.NONE, FileDownloadStatus.UPDATE, FileDownloadStatus.PAUSE, FileDownloadStatus.IMPORT, FileDownloadStatus.ERROR -> {
Log.e("jingo", "开始下载 ${taskBean.status}")
downloadManager.start(taskBean.id)
}
@ -125,6 +127,29 @@ class TaskListAdapter(
View.INVISIBLE
binding.taskDownloadBtn.text = "更新"
}
FileDownloadStatus.IMPORTING -> {
if (binding.taskProgress.visibility != View.VISIBLE) binding.taskProgress.visibility =
View.VISIBLE
binding.taskDownloadBtn.text = "安装中"
val split = taskBean.message.split("/")
if (split.size == 2) {
try {
val index = split[0].toInt()
val count = split[1].toInt()
binding.taskProgress.progress =
index * 100 / count
} catch (e: Exception) {
Log.e("jingo", "更新进度条 $e")
}
} else {
binding.taskProgress.progress = 0
}
}
FileDownloadStatus.IMPORT -> {
if (binding.taskProgress.visibility != View.VISIBLE) binding.taskProgress.visibility =
View.INVISIBLE
binding.taskDownloadBtn.text = "安装"
}
}
}

View File

@ -30,6 +30,7 @@ class TaskListFragment : BaseFragment(){
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
downloadManager.init(requireContext())
_binding = FragmentTaskListBinding.inflate(inflater, container, false)
return binding.root

View File

@ -27,12 +27,28 @@ class TaskListViewModel @Inject constructor(
fun getTaskList(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
val realm = Realm.getDefaultInstance()
Log.e("jingo","realm hashCOde ${realm.hashCode()}")
Log.e("jingo", "realm hashCOde ${realm.hashCode()}")
var taskList: List<TaskBean> = mutableListOf()
when (val result = networkService.getTaskList("02911")) {
is NetResult.Success -> {
if (result.data != null) {
realm.executeTransaction {
realm.copyToRealmOrUpdate(result.data.obj)
result.data.obj?.let { list ->
for (task in list) {
val item = realm.where(TaskBean::class.java).equalTo(
"id", task.id
).findFirst()
if (item != null) {
task.fileSize = item.fileSize
Log.e("jingo", "当前文件大小 ${task.fileSize}")
task.status = item.status
task.currentSize = item.currentSize
}
realm.copyToRealmOrUpdate(task)
}
}
val objects = realm.where(TaskBean::class.java).findAll()
taskList = realm.copyFromRealm(objects)
}
}
}
@ -51,9 +67,8 @@ class TaskListViewModel @Inject constructor(
is NetResult.Loading -> {}
else -> {}
}
val objects = realm.where(TaskBean::class.java).findAll()
val taskList = realm.copyFromRealm(objects)
for(item in taskList){
for (item in taskList) {
FileManager.checkOMDBFileInfo(item)
}
liveDataTaskList.postValue(taskList)

View File

@ -8,6 +8,7 @@
android:paddingLeft="10dp"
android:paddingTop="5dp"
android:paddingRight="10dp"
android:paddingBottom="5dp"
tools:context="com.navinfo.omqs.ui.fragment.tasklist.TaskListAdapter">
<TextView

View File

@ -38,8 +38,8 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
sourceSets {

View File

@ -1549,4 +1549,104 @@
<symbol src="assets:symbols/dot_blue.svg" />
</m>
</m>
<m k="qi_table">
<!-- 道路线 -->
<m v="OMDB_RD_LINK">
<line stroke="#9c9c9c" width="1"/>
</m>
<!--道路方向-->
<m v="OMDB_RD_LINK_DIRECT">
<m k="direct">
<m v="2">
<lineSymbol src="assets:omdb/oneway_left.svg"></lineSymbol>
</m>
<m v="3">
<lineSymbol src="assets:omdb/oneway_right.svg"></lineSymbol>
</m>
</m>
</m>
<!--道路种别-->
<m v="OMDB_RD_LINK_KIND">
<m k="kind">
<m v="1">
<line stroke="#fcacd4" width="1"/>
</m>
<m v="2">
<line stroke="#dcacfc" width="1"/>
</m>
<m v="3">
<line stroke="#fc9c9c" width="1"/>
</m>
<m v="4">
<line stroke="#fcd484" width="1"/>
</m>
<m v="5">
<line stroke="#ecfccc" width="1"/>
</m>
<m v="6">
<line stroke="#acec84" width="1"/>
</m>
<m v="7">
<line stroke="#806048" width="1"/>
</m>
<m v="8">
<line stroke="#fcfc7c" width="1"/>
</m>
<m v="9">
<line stroke="#acc4fc" width="1"/>
</m>
<m v="10">
<line stroke="#8cc8e0" width="1"/>
</m>
<m v="11">
<line stroke="#64ecdc" width="1"/>
</m>
<m v="13">
<line stroke="#585080" width="1"/>
</m>
<m v="15">
<line stroke="#647430" width="1"/>
</m>
</m>
</m>
<!--常规点限速-->
<m v="OMDB_SPEEDLIMIT">
<m k="speedFlag">
<m v="0">
<circle fill="#0000ff" radius="30" scale-radius="true" />
<text k="maxSpeed" use="road"></text>
<text k="minSpeed" dy="30" bg-fill="#00ff00" use="road"></text>
</m>
<m v="1">
<!-- <circle fill="#0000ff" radius="30" scale-radius="true" />-->
<text k="maxSpeed" use="road"></text>
<text k="minSpeed" dy="30" bg-fill="#00ff00" use="road"></text>
</m>
</m>
</m>
<!--条件点限速-->
<m v="OMDB_SPEEDLIMIT_COND">
<m k="direct">
<m v="2">
<lineSymbol src="assets:omdb/oneway_left.svg"></lineSymbol>
</m>
<m v="3">
<lineSymbol src="assets:omdb/oneway_right.svg"></lineSymbol>
</m>
</m>
</m>
<!--可变点限速-->
<m v="OMDB_SPEEDLIMIT_VAR">
<m k="direct">
<m v="2">
<lineSymbol src="assets:omdb/oneway_left.svg"></lineSymbol>
</m>
<m v="3">
<lineSymbol src="assets:omdb/oneway_right.svg"></lineSymbol>
</m>
</m>
</m>
</m>
</rendertheme>

View File

@ -0,0 +1,11 @@
<svg width="580" height="580" xmlns="http://www.w3.org/2000/svg" version="1.0">
<metadata id="metadata10"/>
<g>
<title>Layer 1</title>
<g transform="rotate(-179.925 290 290)" id="svg_1">
<path stroke="#ffffff" shape-rendering="auto" filter-blend-mode="normal" stroke-linejoin="round" fill="none" solid-opacity="1" color-interpolation="sRGB" isolation="auto" stroke-width="64" color-rendering="auto" image-rendering="auto" color="#000000" filter-gaussianBlur-deviation="0" color-interpolation-filters="linearRGB" opacity="0.8" solid-color="#000000" mix-blend-mode="normal" d="m30,261l286,0l0,-72.5l234,101.85l-234,101.15l0,-72.5l-286,0l0,-58z" id="path4151"/>
<path fill="#6a6a6a" id="path4136" d="m30,261l286,0l0,-72.5l234,101.85l-234,101.15l0,-72.5l-286,0l0,-58z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@ -0,0 +1,11 @@
<svg width="580" height="580" xmlns="http://www.w3.org/2000/svg" version="1.0">
<metadata id="metadata10"/>
<g>
<title>Layer 1</title>
<g id="svg_1" transform="rotate(-0.400407 290 290)">
<path id="path4151" d="m30,261l286,0l0,-72.5l234,101.85l-234,101.15l0,-72.5l-286,0l0,-58z" mix-blend-mode="normal" solid-color="#000000" opacity="0.8" color-interpolation-filters="linearRGB" filter-gaussianBlur-deviation="0" color="#000000" image-rendering="auto" color-rendering="auto" stroke-width="64" isolation="auto" color-interpolation="sRGB" solid-opacity="1" fill="none" stroke-linejoin="round" filter-blend-mode="normal" shape-rendering="auto" stroke="#ffffff"/>
<path d="m30,261l286,0l0,-72.5l234,101.85l-234,101.15l0,-72.5l-286,0l0,-58z" id="path4136" fill="#6a6a6a"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 800 B

View File

@ -4,7 +4,6 @@ import com.navinfo.collect.library.utils.GeometryToolsKt
import io.realm.RealmObject
import io.realm.RealmSet
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
/**

View File

@ -116,6 +116,18 @@ open class LayerManagerHandler(context: AppCompatActivity, mapView: NIMapView,tr
mMapView.updateMap()
// initMapLifeSource()
// 设置矢量图层均在12级以上才显示
mMapView.vtmMap.events.bind(UpdateListener { e, mapPosition ->
if (e == org.oscim.map.Map.SCALE_EVENT) {
// 测评数据图层在指定Zoom后开始显示
val isOmdbZoom = mapPosition.zoomLevel>=Constant.OMDB_MIN_ZOOM
baseGroupLayer?.layers?.forEach {
it.isEnabled = !isOmdbZoom
}
omdbVectorTileLayer.isEnabled = isOmdbZoom
omdbLabelLayer.isEnabled = isOmdbZoom
}
})
}
private fun initOMDBVectorTileLayer() {

View File

@ -42,7 +42,7 @@ class GeometryToolsKt {
val tileY1 = MercatorProjection.latitudeToTileY(minMaxY[1], Constant.OVER_ZOOM.toByte())
val minTileY = if (tileY0 <= tileY1) tileY0 else tileY1
val maxTileY = if (tileY0 <= tileY1) tileY1 else tileY0
println("getTileYByGeometry$envelope===$minTileY===$maxTileY")
// println("getTileYByGeometry$envelope===$minTileY===$maxTileY")
for (i in minTileY..maxTileY) {
tileYSet.add(i)
@ -86,7 +86,7 @@ class GeometryToolsKt {
val tileX1 = MercatorProjection.longitudeToTileX(minMaxX[1], Constant.OVER_ZOOM.toByte())
val minTileX = if (tileX0 <= tileX1) tileX0 else tileX1
val maxTileX = if (tileX0 <= tileX1) tileX1 else tileX0
println("getTileXByGeometry$envelope$minTileX===$maxTileX")
// println("getTileXByGeometry$envelope$minTileX===$maxTileX")
for (i in minTileX..maxTileX) {
tileXSet.add(i)
}

View File

@ -0,0 +1,367 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:tns="http://opensciencemap.org/rendertheme"
xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
targetNamespace="http://opensciencemap.org/rendertheme" xml:lang="en">
<!-- attribute types -->
<xs:simpleType name="cap">
<xs:restriction base="xs:string">
<xs:enumeration value="butt"/>
<xs:enumeration value="round"/>
<xs:enumeration value="square"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="closed">
<xs:restriction base="xs:string">
<xs:enumeration value="yes"/>
<xs:enumeration value="no"/>
<xs:enumeration value="any"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="color">
<xs:restriction base="xs:string">
<xs:pattern value="#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="elementList">
<xs:restriction base="xs:string">
<xs:enumeration value="node"/>
<xs:enumeration value="way"/>
<xs:enumeration value="any"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="selectorList">
<xs:restriction base="xs:string">
<xs:enumeration value="first"/>
<xs:enumeration value="any"/>
<xs:enumeration value="when-matched"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="fontFamily">
<xs:restriction base="xs:string">
<xs:enumeration value="default"/>
<xs:enumeration value="default_bold"/>
<xs:enumeration value="monospace"/>
<xs:enumeration value="sans_serif"/>
<xs:enumeration value="serif"/>
<xs:enumeration value="thin"/>
<xs:enumeration value="light"/>
<xs:enumeration value="medium"/>
<xs:enumeration value="black"/>
<xs:enumeration value="condensed"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="fontStyle">
<xs:restriction base="xs:string">
<xs:enumeration value="bold"/>
<xs:enumeration value="bold_italic"/>
<xs:enumeration value="italic"/>
<xs:enumeration value="normal"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="nonNegativeFloat">
<xs:restriction base="xs:float">
<xs:minInclusive value="0"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="src">
<xs:restriction base="xs:string">
<xs:pattern value="((file|assets):)?.+"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="strokeDasharray">
<xs:restriction base="xs:string">
<xs:pattern
value="([0-9]+(\.[0-9]+)? *, *[0-9]+(\.[0-9]+)? *, *)*[0-9]+(\.[0-9]+)? *, *[0-9]+(\.[0-9]+)?"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="textKey">
<xs:restriction base="xs:string">
<xs:enumeration value="ele"/>
<xs:enumeration value="addr:housenumber"/>
<xs:enumeration value="addr_housenumber"/> <!-- Mapzen -->
<xs:enumeration value="housenumber"/> <!-- OpenMapTiles -->
<xs:enumeration value="name"/>
<xs:enumeration value="maxSpeed"/><!--最高速度-->
<xs:enumeration value="minSpeed"/><!--最低速度-->
<xs:enumeration value="ref"/>
</xs:restriction>
</xs:simpleType>
<!-- style menu cat element -->
<xs:complexType name="cat">
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<!-- style menu name element -->
<xs:complexType name="name">
<xs:attribute name="lang" type="xs:string" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:complexType>
<!-- style menu overlay element -->
<xs:complexType name="overlay">
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<!-- style menu layer element -->
<xs:complexType name="layer">
<xs:sequence maxOccurs="1" minOccurs="0">
<xs:element name="name" maxOccurs="unbounded" minOccurs="0" type="tns:name"/>
<xs:element name="cat" maxOccurs="unbounded" minOccurs="0" type="tns:cat"/>
<xs:element name="overlay" maxOccurs="unbounded" minOccurs="0" type="tns:overlay"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="parent" type="xs:string" use="optional"/>
<xs:attribute name="visible" default="false" type="xs:boolean" use="optional"/>
<xs:attribute name="enabled" default="false" type="xs:boolean" use="optional"/>
</xs:complexType>
<!-- rendering instructions -->
<xs:complexType name="area">
<xs:attribute name="cat" type="xs:string" use="optional"/>
<xs:attribute name="id" default="0" type="xs:string" use="optional"/>
<xs:attribute name="use" default="0" type="xs:string" use="optional"/>
<xs:attribute name="src" type="tns:src" use="optional"/>
<xs:attribute name="symbol-width" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-height" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-percent" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="fill" default="#000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke" default="#00000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke-width" default="0" type="tns:nonNegativeFloat" use="optional"/>
<xs:attribute name="fade" default="-1" type="xs:integer" use="optional"/>
<xs:attribute name="blend" default="-1" type="xs:integer" use="optional"/>
<xs:attribute name="blend-fill" default="#000000" type="tns:color" use="optional"/>
<xs:attribute name="mesh" default="false" type="xs:boolean" use="optional"/>
</xs:complexType>
<xs:complexType name="caption">
<xs:attribute name="cat" type="xs:string" use="optional"/>
<xs:attribute name="k" type="tns:textKey" use="required"/>
<xs:attribute name="dy" default="0" type="xs:float" use="optional"/>
<xs:attribute name="font-family" default="default" type="tns:fontFamily" use="optional"/>
<xs:attribute name="style" default="normal" type="tns:fontStyle" use="optional"/>
<xs:attribute name="size" default="0" type="tns:nonNegativeFloat" use="optional"/>
<xs:attribute name="bg-fill" default="#00000000" type="tns:color" use="optional"/>
<xs:attribute name="fill" default="#000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke" default="#000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke-width" default="0" type="tns:nonNegativeFloat" use="optional"/>
<!-- polygon area expressed as a ratio to tile area, e.g. 0.1 for 10% of tile area -->
<xs:attribute name="area-size" default="0" type="tns:nonNegativeFloat" use="optional"/>
<!-- priority for label placement, 0 = highest priority -->
<xs:attribute name="priority" default="0" type="xs:integer" use="optional"/>
<!-- symbol src name -->
<xs:attribute name="symbol" type="tns:src" use="optional"/>
<xs:attribute name="symbol-width" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-height" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-percent" type="xs:positiveInteger" use="optional"/>
</xs:complexType>
<xs:complexType name="circle">
<xs:attribute name="cat" type="xs:string" use="optional"/>
<xs:attribute name="radius" type="tns:nonNegativeFloat" use="required"/>
<xs:attribute name="scale-radius" default="false" type="xs:boolean" use="optional"/>
<xs:attribute name="fill" default="#00000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke" default="#00000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke-width" default="0" type="tns:nonNegativeFloat" use="optional"/>
</xs:complexType>
<xs:complexType name="line">
<xs:attribute name="cat" type="xs:string" use="optional"/>
<!-- style: TODO only in style-line-->
<xs:attribute name="id" default="0" type="xs:string" use="optional"/>
<!-- inherited style -->
<xs:attribute name="use" default="0" type="xs:string" use="optional"/>
<xs:attribute name="src" type="tns:src" use="optional"/>
<xs:attribute name="symbol-width" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-height" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-percent" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="stroke" default="#000000" type="tns:color" use="optional"/>
<xs:attribute name="width" default="0" type="xs:float" use="optional"/>
<!-- minimum scaled width to draw outline -->
<xs:attribute name="min" default="0" type="xs:float" use="optional"/>
<xs:attribute name="dasharray" type="tns:strokeDasharray" use="optional"/>
<xs:attribute name="cap" default="round" type="tns:cap" use="optional"/>
<xs:attribute name="outline" default="" type="xs:string" use="optional"/>
<xs:attribute name="fade" default="-1" type="xs:integer" use="optional"/>
<xs:attribute name="blur" default="-1" type="xs:float" use="optional"/>
<xs:attribute name="fix" default="false" type="xs:boolean" use="optional"/>
<xs:attribute name="repeat-gap" default="200" type="xs:float" use="optional"/>
<xs:attribute name="repeat-start" default="30" type="xs:float" use="optional"/>
<!-- stipple repeat in 'pixel' -->
<xs:attribute name="stipple" default="0" type="xs:integer" use="optional"/>
<!-- stipple color -->
<xs:attribute name="stipple-stroke" default="#000000" type="tns:color" use="optional"/>
<!-- stipple width relative to line width, i.e 0.0-1.0 -->
<xs:attribute name="stipple-width" default="0" type="xs:float" use="optional"/>
</xs:complexType>
<xs:complexType name="text">
<xs:attribute name="cat" type="xs:string" use="optional"/>
<xs:attribute name="id" default="0" type="xs:string" use="optional"/>
<xs:attribute name="use" default="0" type="xs:string" use="optional"/>
<xs:attribute name="k" default="name" type="tns:textKey" use="optional"/>
<xs:attribute name="dy" default="0" type="xs:float" use="optional"/>
<xs:attribute name="font-family" default="default" type="tns:fontFamily" use="optional"/>
<xs:attribute name="style" default="normal" type="tns:fontStyle" use="optional"/>
<xs:attribute name="size" default="0" type="tns:nonNegativeFloat" use="optional"/>
<xs:attribute name="bg-fill" default="#00000000" type="tns:color" use="optional"/>
<xs:attribute name="fill" default="#000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke" default="#000000" type="tns:color" use="optional"/>
<xs:attribute name="stroke-width" default="0" type="tns:nonNegativeFloat" use="optional"/>
<xs:attribute name="caption" default="false" type="xs:boolean" use="optional"/>
<!-- polygon area expressed as a ratio to tile area, e.g. 0.1 for 10% of tile area -->
<xs:attribute name="area-size" default="0" type="tns:nonNegativeFloat" use="optional"/>
<!-- priority for label placement, 0 = highest priority -->
<xs:attribute name="priority" default="0" type="xs:integer" use="optional"/>
</xs:complexType>
<xs:complexType name="symbol">
<xs:attribute name="cat" type="xs:string" use="optional"/>
<xs:attribute name="id" default="0" type="xs:string" use="optional"/>
<xs:attribute name="use" default="0" type="xs:string" use="optional"/>
<xs:attribute name="src" type="tns:src" use="optional"/>
<xs:attribute name="symbol-width" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-height" type="xs:positiveInteger" use="optional"/>
<xs:attribute name="symbol-percent" type="xs:positiveInteger" use="optional"/>
<!-- symbols on lines -->
<xs:attribute name="billboard" default="false" type="xs:boolean" use="optional"/>
<xs:attribute name="repeat" default="false" type="xs:boolean" use="optional"/>
<xs:attribute name="repeat-gap" default="200" type="xs:float" use="optional"/>
<xs:attribute name="repeat-start" default="30" type="xs:float" use="optional"/>
<xs:attribute name="rotate" default="true" type="xs:boolean" use="optional"/>
</xs:complexType>
<xs:complexType name="extrusion">
<xs:attribute name="cat" type="xs:string" use="optional"/>
<xs:attribute name="line-color" type="tns:color" use="optional"/>
<xs:attribute name="side-color" type="tns:color" use="required"/>
<xs:attribute name="top-color" type="tns:color" use="required"/>
<xs:attribute name="hsv-h" default="0" type="xs:double" use="optional"/>
<xs:attribute name="hsv-s" default="1" type="xs:double" use="optional"/>
<xs:attribute name="hsv-v" default="1" type="xs:double" use="optional"/>
<!-- 12m default -->
<xs:attribute name="default-height" default="12" type="xs:positiveInteger" use="optional"/>
</xs:complexType>
<!-- match elements -->
<xs:complexType name="m">
<xs:choice maxOccurs="unbounded" minOccurs="0">
<!-- recursion to allow for nested m -->
<xs:element name="m" type="tns:m"/>
<xs:element name="area" type="tns:area"/>
<xs:element name="caption" type="tns:caption"/>
<xs:element name="circle" type="tns:circle"/>
<xs:element name="line" type="tns:line"/>
<xs:element name="outline" type="tns:line"/>
<xs:element name="lineSymbol" type="tns:line"/>
<xs:element name="text" type="tns:text"/>
<xs:element name="extrusion" type="tns:extrusion"/>
<xs:element name="symbol" type="tns:symbol"/>
<!-- outline is defined within rules to match layering -->
<xs:element name="outline-layer" type="tns:line"/>
</xs:choice>
<xs:attribute name="select" default="any" type="tns:selectorList" use="optional"/>
<xs:attribute name="e" type="tns:elementList" use="optional"/>
<xs:attribute name="k" type="xs:string" use="optional"/>
<xs:attribute name="v" type="xs:string" use="optional"/>
<xs:attribute name="cat" type="xs:string" use="optional"/>
<xs:attribute name="closed" default="any" type="tns:closed" use="optional"/>
<xs:attribute name="zoom-min" default="0" type="xs:unsignedByte" use="optional"/>
<xs:attribute name="zoom-max" default="127" type="xs:unsignedByte" use="optional"/>
</xs:complexType>
<xs:complexType name="atlasRect">
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="pos" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="atlas">
<xs:choice maxOccurs="unbounded" minOccurs="0">
<xs:element name="rect" type="tns:atlasRect"/>
</xs:choice>
<xs:attribute name="img" type="xs:string" use="required"/>
</xs:complexType>
<!-- stylemenu element -->
<xs:complexType name="stylemenu">
<xs:sequence maxOccurs="1" minOccurs="0">
<xs:element name="layer" maxOccurs="unbounded" minOccurs="0" type="tns:layer"/>
</xs:sequence>
<xs:attribute name="defaultvalue" type="xs:string" use="required"/>
<xs:attribute name="defaultlang" type="xs:string" use="required"/>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<!-- tag-transform element -->
<xs:complexType name="tag-transform">
<xs:attribute name="k" type="xs:string" use="required"/>
<xs:attribute name="v" type="xs:string" use="optional"/>
<xs:attribute name="k-lib" type="xs:string" use="required"/>
<xs:attribute name="v-lib" type="xs:string" use="optional"/>
</xs:complexType>
<!-- rendertheme element -->
<xs:complexType name="rendertheme">
<xs:sequence maxOccurs="1" minOccurs="0">
<xs:element name="stylemenu" maxOccurs="1" minOccurs="0" type="tns:stylemenu"/>
<!-- tag definitions -->
<xs:choice maxOccurs="unbounded" minOccurs="0">
<xs:element name="tag-transform" type="tns:tag-transform"/>
</xs:choice>
<!-- style definitions -->
<xs:sequence maxOccurs="256" minOccurs="0">
<xs:choice maxOccurs="unbounded" minOccurs="0">
<xs:element name="style-text" type="tns:text"/>
<xs:element name="style-symbol" type="tns:symbol"/>
<xs:element name="style-area" type="tns:area"/>
<xs:element name="style-line" type="tns:line"/>
<!-- <xs:element name="style-outline" type="tns:line" /> -->
</xs:choice>
</xs:sequence>
<xs:choice maxOccurs="1" minOccurs="0">
<xs:element name="atlas" type="tns:atlas"/>
</xs:choice>
<!-- matching rules -->
<xs:sequence maxOccurs="unbounded" minOccurs="0">
<xs:element name="m" type="tns:m"/>
</xs:sequence>
</xs:sequence>
<xs:attribute name="version" type="xs:positiveInteger" use="required"/>
<xs:attribute name="map-background" default="#ffffff" type="tns:color" use="optional"/>
<xs:attribute name="base-stroke-width" default="1" type="tns:nonNegativeFloat"
use="optional"/>
<xs:attribute name="base-text-scale" default="1" type="tns:nonNegativeFloat"
use="optional"/>
</xs:complexType>
<!-- root element -->
<xs:element name="rendertheme" type="tns:rendertheme"/>
</xs:schema>