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

目录

写在开头

正文

一、界面布局

二、功能实现

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”标签里写上我们整个界面的设计代码,如下:

<com.esri.arcgisruntime.mapping.view.MapView
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:orientation="horizontal"
                android:paddingLeft="20dp"
                android:paddingRight="20dp">

                <Button
                    android:id="@+id/button1"
                    android:layout_width="20dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="栅格" />

                <Button
                    android:id="@+id/button2"
                    android:layout_width="20dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="矢量" />
                <Button
                    android:id="@+id/button3"
                    android:layout_width="20dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="在线" />
            </LinearLayout>

        <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:text="基站信息" />
        <TextView
            android:id="@+id/lacationText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:text="地理位置" />
        </LinearLayout>
</com.esri.arcgisruntime.mapping.view.MapView>

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渲染):

<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:glEsVersion="0x00020000" android:required="true" />

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

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

注意:导航权限开启许可函数(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<CellInfo> 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标签写入代码如下:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

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

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

useLibrary'org.apache.http.legacy'

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

4.点击按钮,app就闪退

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

<uses-library android:name="org.apache.http.legacy" android:required="false"/>

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

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

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

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

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="system" />
            <certificates src="user" />
        </trust-anchors>
    </base-config>
</network-security-config>

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值