移动GIS开发:手机基站定位+离线切片地图(矢量vtpk+栅格tpk)导航安卓APP

您所在的位置:网站首页 手机切片app 移动GIS开发:手机基站定位+离线切片地图(矢量vtpk+栅格tpk)导航安卓APP

移动GIS开发:手机基站定位+离线切片地图(矢量vtpk+栅格tpk)导航安卓APP

2023-12-23 07:43| 来源: 网络整理| 查看: 265

目录

写在开头

正文

一、界面布局

二、功能实现

1.显示在线地图和定位

2.基站信息的获取和显示

3.地理信息的获取和解析

4.显示栅格离线地图(.tpk)

5.显示矢量离线地图(.vtpk)

三、主要问题及解决方案

写在开头

这个APP的开发是我们这学期的一门课的期末大作业,要求主要有三点:显示栅格离线地图切片(.tpk),显示矢量离线地图切片(.vtpk),基于基站实现手机定位。

作为第一次接触安卓开发的小白,从Android Studio的安装到最终实现功能,这中间的每一步我都走得很艰难。做APP的这一周我遇到大大小小很多问题,也重写了很多次代码。无数次想要放弃,但每次都告诉自己再坚持一下看能不能解决,最终成功做出了这个APP,可喜可贺。其实这个APP并不完善,但是第一次零基础做到这样,我已经很满意了。这里主要记录一下APP的实现过程以及自己遇到的各种问题和解决办法,希望对有需要的小伙伴有帮助。

正文

以实现功能为主,本文主要展示核心代码,关于代码细节和一些原理并未详细描述。

一、界面布局

根据功能设计,首先我的界面需要一个ArcGIS中的MapView,用来显示地图;还需要三个按钮,分别用来切换显示本地栅格、本地矢量、在线地图;另外需要两个TextView,分别用来显示基站信息和地理信息。

我的界面大概就长下面这样:

整个界面的结构是MapView在最外层,如下:

上述界面是直接用代码实现的。如果不用代码而是要手动调整的话,我也不知道怎么实现。下面说说实现具体步骤:

1.在如下图的位置中找到“res”文件夹下的“layout”文件夹里的一个叫做“activity_main.xml”的文件:

2.打开上述xml文件,第一次应该首先看的是设计界面,在右上角点击Code切换到代码界面:

3.在“RelativeLayout”标签里写上我们整个界面的设计代码,如下:

4.返回到设计界面,可以查看界面是否是上述的布局。

5.在MainActivity.java的“onCreate”里添加对上述三个按钮的点击事件,主要实现按钮上文本的变化,代码如下:

btngrid.setOnClickListener(new View.OnClickListener() {//栅格按钮 @Override public void onClick(View view) { btngrid.setText("栅格(now)"); btnvector.setText("矢量"); btnonline.setText("在线"); } }); btnvector.setOnClickListener(new View.OnClickListener() {//矢量按钮 @Override public void onClick(View view) { btnvector.setText("矢量(now)"); btngrid.setText("栅格"); btnonline.setText("在线"); } }); btnonline.setOnClickListener(new View.OnClickListener() {//在线按钮 @Override public void onClick(View view) { btnonline.setText("在线(now)"); btnvector.setText("矢量"); btngrid.setText("栅格"); } }); 二、功能实现 1.显示在线地图和定位

在线地图和定位的显示直接参考ArcGIS的安卓开发教程。

在线地图显示:https://developers.arcgis.com/labs/android/create-a-starter-app/

定位显示:https://developers.arcgis.com/labs/android/display-and-track-your-location/

下面贴一下自己程序中的代码。

首先是在线地图显示函数。其中Basemap的Type可以改的,我这里是栅格地图,你也可以设置成矢量图、街景图等等。代码如下:

//显示在线地图 private void setupMap() { if (mMapView != null) { Basemap.Type basemapType = Basemap.Type.IMAGERY_WITH_LABELS; double latitude = 34.09042; double longitude = -118.71511; int levelOfDetail = 6; map = new ArcGISMap(basemapType, latitude, longitude, levelOfDetail); mMapView.setMap(map); } }

重写三个函数,以正确显示在线地图,直接写在“MainActivity”类中。代码如下:

@Override protected void onPause() { if (mMapView != null) { mMapView.pause(); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (mMapView != null) { mMapView.resume(); } } @Override protected void onDestroy() { if (mMapView != null) { mMapView.dispose(); } super.onDestroy(); }

定位显示(功能实现函数+导航权限开启许可函数):

//位置导航 private void setupLocationDisplay() { mLocationDisplay = mMapView.getLocationDisplay(); mLocationDisplay.addDataSourceStatusChangedListener(dataSourceStatusChangedEvent -> { // If LocationDisplay started OK or no error is reported, then continue. if (dataSourceStatusChangedEvent.isStarted() || dataSourceStatusChangedEvent.getError() == null) { return; } int requestPermissionsCode = 2; String[] requestPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}; // If an error is found, handle the failure to start. // Check permissions to see if failure may be due to lack of permissions. if (!(ContextCompat.checkSelfPermission(MainActivity.this, requestPermissions[0]) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(MainActivity.this, requestPermissions[1]) == PackageManager.PERMISSION_GRANTED)) { // If permissions are not already granted, request permission from the user. ActivityCompat.requestPermissions(MainActivity.this, requestPermissions, requestPermissionsCode); } else { // Report other unknown failure types to the user - for example, location services may not // be enabled on the device. String message = String.format("Error in DataSourceStatusChangedListener: %s", dataSourceStatusChangedEvent .getSource().getLocationDataSource().getError().getMessage()); Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); } }); mLocationDisplay.setAutoPanMode(LocationDisplay.AutoPanMode.COMPASS_NAVIGATION); mLocationDisplay.startAsync(); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Location permission was granted. This would have been triggered in response to failing to start the // LocationDisplay, so try starting this again. mLocationDisplay.startAsync(); } else { // If permission was denied, show toast to inform user what was chosen. If LocationDisplay is started again, // request permission UX will be shown again, option should be shown to allow never showing the UX again. // Alternative would be to disable functionality so request is not shown again. Toast.makeText(MainActivity.this, getResources().getString(R.string.location_permission_denied), Toast.LENGTH_SHORT).show(); } }

 调用方法:“在线地图显示”和“定位显示”在MainActivity.java的“onCreate”里调用,保证界面打开就能展示;另外“在线地图显示”在“在线按钮”中再调用一次,用于切换地图。OnCreate里调用代码:

mMapView = findViewById(R.id.mapView); setupMap(); setupLocationDisplay();

最后,需要在app > manifests > AndroidManifest.xml里开启权限。

在线地图显示权限(网络访问+OpenGL渲染):

导航显示权限(GPS位置):

注意:导航权限开启许可函数(onRequestPermissionsResult)和三个Override的函数不需要调用,直接执行上述操作就行了。

效果图(在线地图+导航)展示如下。这是最终成果图,所以有基站信息和位置信息,我先马赛克掉了。

2.基站信息的获取和显示

先来了解一下什么是基站信息。基站信息主要指以下四个参数:

mcc:国家代码(中国为460)

mnc:网络类型(移动为0,联通为1,电信为sid)

lac:位置区域码

cid: 基站编号

上述基站信息是存储在数据库中的,我们只需要调用函数,它便能根据手机地址在数据库中查询所需信息。网上有很多现成的相关代码,我的代码参考了Android实现基站定位一文。

首先用一个结构体来存储基站信息,结构体构建如下:

/** 基站信息结构体 */ public class SCell { public int MCC; public int MNC; public int LAC; public int CID; }

下面是获取基站信息的代码:

//获取基站信息 private String getBaseStationInformation(){ StringBuffer sbtv = new StringBuffer(); cell = new SCell();//基站信息 List all_cell_info;//所有基站信息 sbtv.append("基站信息"+"\r\n"); if (mTelephonyManager == null) { mTelephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE); } // 返回值MCC + MNC String operator = mTelephonyManager.getNetworkOperator(); if (operator != null && operator.length() > 3) { cell.MCC = Integer.parseInt(operator.substring(0, 3)); cell.MNC = Integer.parseInt(operator.substring(3)); sbtv.append("mcc:" + cell.MCC + ";mnc:" + cell.MNC + ";\r\n"); } // 获取邻区基站信息 if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { all_cell_info = mTelephonyManager.getAllCellInfo(); sbtv.append("基站数量:" + all_cell_info.size() + "\r\n"); CellInfo cellInfo = all_cell_info.get(0); GsmCellLocation location = (GsmCellLocation) mTelephonyManager.getCellLocation(); if (location == null) Toast.makeText(getApplicationContext(), "获取基站信息失败", Toast.LENGTH_SHORT).show(); cell.CID = location.getCid(); cell.LAC = location.getLac(); sbtv.append("最近基站:"+"cid:" + cell.CID + ";lac:" + cell.LAC + ";\r\n"); } else { Toast.makeText(getApplicationContext(), "请检查定位是否开启", Toast.LENGTH_SHORT).show(); } return sbtv.toString(); }

上述函数将返回一个字符串文本,包含四个基站信息,在OnCreate方法里调用该函数,将结果显示在TextView里。调用代码如下:

text1 = findViewById(R.id.textView); text1.setText(getBaseStationInformation());

 效果图展示:

3.地理信息的获取和解析

这一步对我来说应该最难实现、耗时最多的一个环节了,因为里面用到一些HTTP的接口,还用到了子线程。另外,这一步遇到一个具大的问题就是:获取地理信息和在线地图显示两者居然不能同时实现!具体原因及解决思路在第三个部分-“问题及解决方案”中再详细探讨。这里主要说说如何实现地理信息的获取。

获取到的地理信息包括:经纬度和地址。

获取地理信息的思路:通过GET请求,利用四个基站信息在接口地址查询,返回数据的格式为CSV/JSON/XML。具体的接口说明和参数说明可以访问这个网址:接口说明文档

这里给一个访问接口的例子:http://api.cellocation.com:81/cell/?mcc=460&mnc=1&lac=4301&ci=20986&output=csv。返回的数据如下:

0,40.008899,116.483642,903,"北京市朝阳区望京开发街道屏翠东路;利泽东街与屏翠东路路口东189米"

这是一串字符串,我们最终只需要显示经纬度和地理位置,其余的信息就不用管了。

知道了思路,下面说一下如何用代码来获取信息。代码主要包括两个函数:函数getStringInfo用来访问接口,返回上述的字符串;函数getLocaionInfo用来解析上述字符串,从中剔出经纬度和地址存储在结构体中。

地理信息存储结构体构建如下:

/** 经纬度和地理位置信息结构体 */ public class SLocationInfo { public String latitude; public String longitude; public String address; }

函数getStringInfo:

/** get网站经纬度和地理位置信息 */ private String getStringInfo(String domain) throws Exception { String resultString = ""; /** 采用Android默认的HttpClient */ HttpClient client = new DefaultHttpClient(); /** 采用GET方法 */ HttpGet get = new HttpGet(domain); try { /** 发起GET请求并获得返回数据 */ HttpResponse response = client.execute(get); HttpEntity entity = response.getEntity(); BufferedReader buffReader = new BufferedReader(new InputStreamReader(entity.getContent())); StringBuffer strBuff = new StringBuffer(); String result = null; while ((result = buffReader.readLine()) != null) { strBuff.append(result); } resultString = strBuff.toString(); } catch (Exception e) { Log.e(e.getMessage(), e.toString()); throw new Exception("获取经纬度和地理位置出现错误:"+e.getMessage()); } finally{ get.abort(); client = null; } return resultString; }

 函数getLocaionInfo(在这里面调用上面的函数getStringInfo):

//解析获取的经纬度和地理位置信息 private String getLocaionInfo(String url) throws Exception { locationinfo = new SLocationInfo(); StringBuffer sblocationinfo = new StringBuffer(); String resultString = ""; resultString = getStringInfo(url); sblocationinfo.append("位置信息"+"\r\n"); /** 解析基站位置 */ try{ String errcode = ""; String[] arr = resultString.split(","); errcode = arr[0]; if (errcode.equals("0")){ locationinfo.latitude = arr[1]; locationinfo.longitude = arr[2]; locationinfo.address = arr[4]; sblocationinfo.append("经度:"+locationinfo.longitude+";纬度:"+locationinfo.latitude+";地理位置:"+locationinfo.address+"\r\n"); } } catch (Exception e) { Log.e(e.getMessage(), e.toString()); throw new Exception("获解析地理位置出现错误:"+e.getMessage()); } return sblocationinfo.toString(); }

函数getLocaionInfo需要单独开设一个子线程来调用,因为访问网站是很耗时间的,子线程可以大大减少主线程的负担。但是在这个处理网站访问的子线程里不能处理UI,也就是在TextView里显示地理信息这一操作不能在子线程中实现,因此还需要使用一个UI子线程。前面说得可能有点绕,简单来说就是:网站访问子线程用于处理网站访问操作,当成功获得返回数据后,给UI子线程发送一个成功信号,然后UI子线程用于刷新界面,显示获取的数据。下面直接上代码:

定义用于通知UI子线程的成功信号和失败信号:

private static final int MSG_SUCCESS = 0;// 获取成功的标识 private static final int MSG_FAILURE = 1;// 获取失败的标识

网站访问子线程runnable:

private Runnable runnable = new Runnable() { // 重写run()方法,此方法在新的线程中运行,用于访问网站获取经纬度和地理位置 @Override public void run() { try { locationText = getLocaionInfo(url); } catch (Exception e) { e.printStackTrace(); handler.obtainMessage(MSG_FAILURE).sendToTarget();//给UI子线程发送失败信息 } finally { handler.obtainMessage(MSG_SUCCESS).sendToTarget();//给UI子线程发送成功信息 } } };

UI子线程handler:

private Handler handler = new Handler() { //UI子线程,用于显示经纬度和地理位置信息 @Override public void handleMessage(Message msg) { try{ switch (msg.what){ case MSG_SUCCESS: text2.setText(locationText); Log.e("Success", "解析基站位置成功"); break; case MSG_FAILURE: Log.e("Failure", "解析基站位置失败"); break; } }catch (Exception e) { e.printStackTrace(); } } };

 在OnCreate中运行子线程:

url = "http://api.cellocation.com:81/cell/?mcc=" + cell.MCC + "&mnc=" + cell.MNC + "&lac=" + cell.LAC + "&ci=" + cell.CID + "&output=csv"; text2 = findViewById(R.id.lacationText); try { mThread = new Thread(runnable); mThread.start(); } catch (Exception e) { Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show(); }

效果图展示:

4.显示栅格离线地图(.tpk)

tpk数据来源:由影像图通过ArcGIS制作而成,具体教程网上很多,大家可以自行搜索。 

本地栅格图的显示有两个注意点:一是数据存储位置,二是数据范围。首先是数据存储位置,我使用的绝对位置,所以只有真机测试才能打开数据,如果用模拟器的话就要用其他方式读取数据存储位置了。数据范围一定要包含自己测试所用手机的GPS位置,如果没有,那么就无法显示地图。具体代码比较简单,如下:

定义绝对路径和栅格地图:

private String dirpath = Environment.getExternalStorageDirectory().getAbsolutePath();//本地数据路径 private ArcGISTiledLayer localTiledLayer;//本地栅格地图

本地栅格显示函数:

//打开本地栅格 private void openMyTPK(){ Toast.makeText(getApplicationContext(), dirpath + "/ArcGIS" + "/santai.tpk", Toast.LENGTH_SHORT).show(); String fileLayer = dirpath+"/ArcGIS"+"/santai.tpk"; localTiledLayer = new ArcGISTiledLayer(fileLayer); baseMap = new Basemap(localTiledLayer); map = new ArcGISMap(baseMap); mMapView.setMap(map); }

 在“栅格”按钮里调用上述函数,效果图展示如下:

5.显示矢量离线地图(.vtpk)

vtpk数据来源:网上下载。

矢量离线地图显示的代码和前面的栅格地图几乎一样,唯一不一样的地方就是定义图层的格式不同。栅格是ArcGISTiledLayer,矢量是ArcGISVectorTiledLayer。下面是代码:

//打开本地矢量地图 private void openMyVTPK() { try { Toast.makeText(getApplicationContext(), dirpath+"/ArcGIS"+"/china.vtpk", Toast.LENGTH_SHORT).show(); String fileLayer = dirpath+"/ArcGIS"+"/china.vtpk"; vTiledLayer = new ArcGISVectorTiledLayer(fileLayer); baseMap = new Basemap(vTiledLayer); map = new ArcGISMap(baseMap); mMapView.setMap(map); } catch (Exception e) { String eResult = e.getMessage(); Toast.makeText(MainActivity.this, eResult, Toast.LENGTH_SHORT).show(); } }

效果图展示: 

三、主要问题及解决方案

1.用安卓模拟器运行app时出现找不到device的错误

这是因为电脑BIOS中的虚拟技术没有打开,重启电脑按F2(因电脑而异,我的是联想)进入BIOS打开就行了。教程网上搜索一下就有,需要注意的是进入BIOS界面的时间是电脑开机后刚刚亮的那个瞬间,早了晚了都进不去,我重启了四次才进去。。。

2.本地地图显示不出来

按照我的代码,要想正确显示本地地图一定要在真机上测试,因为是绝对路径。路径是:本记SD卡->ArcGIS(自己建的文件夹)->xxx.tpk/xxx.vtpk。还要注意两点:1.保证离线地图上有自己手机目前的位置;2.开启读写权限,在AndroidManifest.xml的manifest标签写入代码如下:

3.Get请求中的代码找不到对应的方法,例如:DefaultHttpClient

这和Android的SDK版本有关,我的编译版本是28,最小版本是19。在我使用的版本中安卓弃用了上述的一些方法,网上有说更换版本的,但是很麻烦不说还会造成其他代码的冲突。其实在build.gradle(app)中加入所需的包就行了,在android块中写入:

useLibrary'org.apache.http.legacy'

然后点击右上角的“Sync Now”,这样就可以正常使用Get方法了。

4.点击按钮,app就闪退

原因不太清楚,但解决办法是:在AndroidManifest.xml的application标签下添加如下代码:

5.Get获取地理信息与在线地图显示不能同时实现

原因:获取地理信息的网址是Http协议,即明文协议,而在线地图是Https协议,Android Studio默认Https协议。不做任何设置的话,能显示在线地图,但是不能获取到地理信息。

解决方法:添加一个xml文件,作为安全协议设置,保证Http和Https都能访问。

具体步骤:在res文件夹下新建xml文件夹,然后在xml文件夹下新建一个名为“network_security_config.xml”的文件,在里面写入以下代码:

6.运行app时出现闪退现象

这个可能的原因有很多。我自己是在运行app前没有提前打开GPS,解决办法就是要记得提前打开GPS。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3