65.9K
CodeProject 正在变化。 阅读更多。
Home

将设备地理定位并存储坐标到Web服务器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2015 年 6 月 19 日

CPOL

9分钟阅读

viewsIcon

27983

将设备地理定位并存储坐标到Web服务器

引言

在本文中,我们将介绍如何制作一个简单的 Windows Phone 地理定位应用,以及如何将其获取的坐标存储在远程 Web 服务器上以供进一步检查。要制作 Windows Phone 应用,我们将使用 C#,而 Web 端将使用 PHP + MySQL 实现,以便在所有系统上可用。

必备组件

  • Windows 8.1
  • Visual Studio 和 Windows Phone 8.1 SDK
  • 本地或托管的 Web 服务器,带有启用了 PHP 支持的 Apache,以及一个 MySQL 数据库(phpMyAdmin 可能也很有用)。或者,Web 服务器部分也可以用 ASP/ASP.NET 编写。

配置上述每个先决条件超出了本文的范围,因此我将不讨论这一点,假设一切都已成功配置并运行。

如果您需要有关如何安装 Apache 加 PHP/MySQL 服务器的简要参考,可以查阅我的文章 快速在 Debian 或 Ubuntu Linux 上安装 LAMP 服务器

分析场景

为了演示地理定位功能,我们将尽量保持简单。请记住,有很多安全方面的内容我们在这里不会介绍(但也许会在下一篇文章中介绍),因此您所读到的内容旨在提供一个普遍的见解,或者为进一步开发解决方案提供一个起点。我们这里考虑的内容始于一个简单的概念。我们有一个设备(在我这里是智能手机),它具有 GPS 传感器和网络功能,必须通过互联网传输数据。另一方面,我们将在 Web 服务器上运行一个程序,等待被调用以接收数据并保存。接下来,我们将创建一个 C# 应用,该应用能够获取设备的地理位置(以纬度/经度表示),并将这些数据发送到特定的 URI。对于服务器端,我们将创建一个 PHP 脚本,该脚本将读取发送的数据作为 GET 参数,进行处理,然后将其保存在 MySQL 表中。

数据库设置

首先,我们必须创建一个表来存储收集到的数据。以下是我们可以在 phpMyAdmin 中运行的用于创建表的 T-SQL 脚本。我创建了一个名为“geolog”的新数据库,并在其中创建了一个名为“entries”的表。在这里,我们将存储智能手机发送的所有数据,即纬度/经度、设备名称(以防我们要跟踪多个设备,按名称选择每个设备)和用户注解。其余字段(IdEntryTimeStamp)将由列上的默认约束自动填充。IdEntry 是一个自动递增字段,而 TimeStamp 将从 INSERT 操作发生时的当前时间戳获取其默认值。

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";    
SET time_zone = "+00:00";    
     
CREATE TABLE entries (    
  IdEntry int(9) NOT NULL AUTO_INCREMENT,    
  `TimeStamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,    
  Latitude decimal(12,8) NOT NULL DEFAULT '0.00000000',    
  Longitude decimal(12,8) NOT NULL DEFAULT '0.00000000',    
  Device varchar(50) NOT NULL DEFAULT '',    
  Annotation text NOT NULL,    
  PRIMARY KEY (IdEntry),    
  KEY Idx_Device (Device),    
  KEY Idx_Entry (IdEntry,`TimeStamp`)    
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; 

table

现在,在我们的 MySQL 实例上,我们应该能看到空的表,并且可以开始编写一些 PHP 来完成服务器端的工作。我们将创建一个脚本来接收外部数据并将其写入我们的表,以及第二个脚本来查询数据。让我们开始创建一个名为 *index.php* 的 PHP 页面。

使用 PHP 存储数据

*Index.php* 必须执行非常简单的任务,即接收给定的 GET 请求,读取每个 GET 参数以执行 T-SQL INSERT 操作到“entries”表中,以存储传递的值。再次注意,我没有实现任何安全措施(除了使用准备好的语句,这相当于 ASP.NET 中的参数化查询)。如果您希望在实际场景中使用这些页面,最好先对其进行保护。

代码极简且不言自明。首先,我们将打开数据库连接,然后继续读取 GET 参数并执行我们的 INSERT,最后关闭连接。目前仅此而已。

<?php    
    
   //-- Connecting to "geolog" database, on "localhost" server: 
   //replace <USER> and <PASSWORD> variables with the ones which corresponds 
   //to your MySQL installation    
    
   $mysqli = new mysqli('localhost', '<USER>', '<PASSWORD>', 'geolog');    
    
   if ($mysqli->connect_errno) {    
    
 $mysqli->connect_errno . ") " . $mysqli->connect_error;    
    
      exit();      
   }    
    
   //-- Preparing parametrized INSERT    
    
   if (!($stmt = $mysqli->prepare("INSERT INTO entries(Latitude, Longitude, Device, Annotation) _
      VALUES (?, ?, ?, ?)"))) {    
    
 $mysqli->errno . ") " . $mysqli->error;        
   }        
     
   //-- Acquire GET parameters
   $latitude   = $_GET['lt'];      
   $longitude  = $_GET['ln'];    
   $device     = $_GET['d'];    
   $annotation = $_GET['n'];
        
    
   //-- Bind GET parameters to prepared statement's variables    
    
   if (!$stmt->bind_param("ddss", $latitude, $longitude, $device, $annotation)) {    
 $stmt->errno . ") " . $stmt->error;        
   }        
         
   //-- Execute INSERT query   
    
   if (!$stmt->execute()) {    
 $stmt->errno . ") " . $stmt->error;    
   }         
         
   //-- Closing connection / cleanup    
   $stmt->close();        
   mysqli_close($mysqli);        
?>   

实践中,这意味着如果我们打开一个浏览器并调用一个格式正确的 URL,我们最终就会将数据写入数据库。让我们看一个例子,假设我们的 Web 服务器运行在 localhost 上。

webserver

您在地址栏中看到的 */test* 子目录是我为托管 Web 应用程序在 Apache 上创建的虚拟目录,它可以是您选择的任何名称。有关 VirtualHosts 配置的更多信息,请参阅 Apache 的在线文档。

将地理坐标发送到 Web 服务器

现在是时候编写我们的 Windows Phone 8.1 应用了。在源代码中,您可以找到标记为“FollowMe”的项目。它将包含两个页面,一个用于签到部分,另一个用于应用的设置,例如用于存储数据的查询 URI。稍后会详细介绍。我们的应用必须执行我们在上面手动完成的一些操作,即计算设备的位置并调用一个格式正确的 URL(我们的 Web 应用程序在那里等待)。

让我们从设置页面开始。它将包含一个 TextBox 来指定要查询的 URI,以及一个 ComboBox 来指示发送坐标时使用的区域设置。我们可以像这样编写它:

<Page  
    x:Class="FollowMe.Settings"  
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
    xmlns:local="using:FollowMe"  
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"  
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
    mc:Ignorable="d"  
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"  
    Loaded="Page_Loaded">  
    <Page.BottomAppBar>  
        <CommandBar>  
                <AppBarButton Icon="Accept" Label="Save" Click="AppBarButton_Click"/>  
            <AppBarButton Icon="Cancel" Label="Cancel" Click="CancelButton_Click"/>  
</CommandBar>  
    </Page.BottomAppBar>  
  
    <Grid x:Name="LayoutRoot">  
  
        <Grid.ChildrenTransitions>  
            <TransitionCollection>  
                <EntranceThemeTransition/>  
            </TransitionCollection>  
        </Grid.ChildrenTransitions>  
  
        <Grid.RowDefinitions>  
            <RowDefinition Height="Auto"/>  
            <RowDefinition Height="*"/>  
            </Grid.RowDefinitions>  
  
            <!-- Title Panel -->  
            <StackPanel Grid.Row="0" Margin="19,0,0,0">  
            <TextBlock Text="FollowMe" 
            Style="{ThemeResource TitleTextBlockStyle}" 
            Margin="0,12,0,0"/>  
            <TextBlock Text="Settings" 
            Margin="0,-6.5,0,26.5" Style="{ThemeResource HeaderTextBlockStyle}" 
            CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}"/>  
            </StackPanel>  
  
            <TextBlock HorizontalAlignment="Left" 
            Margin="19,9.833,0,0" Grid.Row="1"TextWrapping="Wrap" 
            Text="Web service URI" VerticalAlignment="Top" 
            FontFamily="Segoe WP"FontSize="18"/>  
            <TextBox x:Name="txtWebUri" 
            HorizontalAlignment="Left" Margin="19,38.833,0,0" 
            Grid.Row="1"TextWrapping="Wrap" VerticalAlignment="Top" 
            Width="362" PlaceholderText="https:///"/>  
            <TextBlock HorizontalAlignment="Left" Margin="19,103.833,0,0" 
            Grid.Row="1"TextWrapping="Wrap" Text="Culture for coordinates" 
            VerticalAlignment="Top" FontFamily="Segoe WP" FontSize="18"/>  
            <ComboBox x:Name="cmbLang" HorizontalAlignment="Left" 
            Margin="19,122.833,0,0" 
            Grid.Row="1"VerticalAlignment="Top" Width="182">  
            <x:String>it-IT</x:String>  
            <x:String>en-US</x:String>  
        </ComboBox>  
    </Grid>  
</Page>  

output

请注意,我添加了一个 CommandBar 来管理两个 AppBarButton,一个用于保存更改,另一个用于取消(并简单关闭页面)。

我们页面的代码隐藏将是:

using FollowMe.Common;  
using System;  
using Windows.Storage;  
using Windows.UI.Xaml;  
using Windows.UI.Xaml.Controls;  
using Windows.UI.Xaml.Navigation;  
  
namespace FollowMe  
{    
    public sealed partial class Settings : Page  
    {  
        private NavigationHelper navigationHelper;  
        private ObservableDictionary defaultViewModel = new ObservableDictionary();  
  
        public Settings()  
        {  
            this.InitializeComponent();  
  
            this.navigationHelper = new NavigationHelper(this);  
            this.navigationHelper.LoadState += this.NavigationHelper_LoadState;  
            this.navigationHelper.SaveState += this.NavigationHelper_SaveState;  
        }  
  
        private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)  
        {  
        }  
  
        private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)  
        {  
        }  
  
        public NavigationHelper NavigationHelper  
        {  
            get { return this.navigationHelper; }  
        }  
  
        public ObservableDictionary DefaultViewModel  
        {  
            get { return this.defaultViewModel; }  
        }  
 
 
        #region NavigationHelper registration  
  
        protected override void OnNavigatedTo(NavigationEventArgs e)  
        {  
            this.navigationHelper.OnNavigatedTo(e);  
        }  
  
        protected override void OnNavigatedFrom(NavigationEventArgs e)  
        {  
            this.navigationHelper.OnNavigatedFrom(e);  
        }  
 
        #endregion  
  
        private void AppBarButton_Click(object sender, RoutedEventArgs e)  
        {  
            var ap = ApplicationData.Current.LocalSettings;  
            ap.Values["WebURI"] = txtWebUri.Text;  
            ap.Values["Language"] = cmbLang.SelectedValue.ToString();  
  
            Frame.Navigate(typeof(MainPage));  
        }  
  
        private void Page_Loaded(object sender, RoutedEventArgs e)  
        {  
            var ap = ApplicationData.Current.LocalSettings;  
            try  
            {  
                txtWebUri.Text = ap.Values["WebURI"].ToString();  
                cmbLang.SelectedValue = ap.Values["Language"].ToString();  
            }  
            catch {  
            }  
        }  
  
        private void CancelButton_Click(object sender, RoutedEventArgs e)  
        {  
            Frame.Navigate(typeof(MainPage));  
        }  
    }  
}  

简单来说,设置页面的逻辑围绕着两个 AppBarButton 的 Click 事件。取消按钮只需导航回 MainPage,而保存按钮会将数据写入我们应用(来自 Windows.Storage 命名空间)的 LocalSettings。为 txtWebUri TextBoxcmbLang ComboBox 指定的参数也会导航回 MainPage

MainPage 页面是应用的核心。它简单地包含一个按钮、一个用于用户可能想要输入的任何备注的 TextBox,以及两个 TextBlock,我们在其中公开地理坐标(仅用于调试目的,TextBlock 实际上不是必需的)。让我们看看 MainPage 的 XAML:

<Page  
    x:Class="FollowMe.MainPage"  
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
    xmlns:local="using:FollowMe"  
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"  
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
    mc:Ignorable="d">  
    <Page.BottomAppBar>  
    <CommandBar>  
        <AppBarButton Icon="Manage" Label="Settings" Click="AppBarButton_Click"/>  
    </CommandBar>  
</Page.BottomAppBar>  
  
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">  
        <Button Content="Check in" Click="button1_Click"  
HorizontalAlignment="Left" Margin="10,381,0,0" Name="button1" 
VerticalAlignment="Top"Height="117" Width="380" />  
  
        <TextBlock HorizontalAlignment="Left" Margin="10,10,0,0" 
        TextWrapping="Wrap"Text="FollowMe" VerticalAlignment="Top" 
        FontFamily="Segoe WP" FontSize="28" FontWeight="Bold"/>  
        <TextBox Name="txtNotes" HorizontalAlignment="Left" 
        Margin="10,69,0,0" TextWrapping="Wrap"VerticalAlignment="Top" 
        Width="380" PlaceholderText="Type any notes here"/>  
  
        <TextBlock HorizontalAlignment="Left" Margin="10,130,0,0" 
        TextWrapping="Wrap"Text="Latitude" VerticalAlignment="Top"/>  
        <TextBlock HorizontalAlignment="Left" Margin="10,152,0,0" 
        TextWrapping="Wrap"Text="Longitude" VerticalAlignment="Top"/>  
        <TextBlock x:Name="lblLatitude" HorizontalAlignment="Left" 
        Margin="104,130,0,0"TextWrapping="Wrap" Text="0.00000000" 
        VerticalAlignment="Top"/>  
        <TextBlock x:Name="lblLongitude" HorizontalAlignment="Left" 
        Margin="104,152,0,0"TextWrapping="Wrap" Text="0.00000000" 
        VerticalAlignment="Top"/>  
  
    <Grid>  
</Page>  

一个 AppBarButton 将允许我们访问 Settings 页面,这里没有其他特殊之处。唯一需要我们考虑代码隐藏的是页面上唯一 ButtonClick 事件。

using System;  
using Windows.UI.Xaml;  
using Windows.UI.Xaml.Controls;  
using Windows.Devices.Geolocation;  
using Windows.Web.Http;  
using Windows.UI.Popups;  
using System.Globalization;  
using Windows.Security.ExchangeActiveSyncProvisioning;  
using Windows.Storage;  
  
namespace FollowMe  
{  
    public sealed partial class MainPage : Page  
    {  
        Geolocator geo = null;  
  
        public MainPage()  
        {  
            this.InitializeComponent();  
        }  
  
        private async void button1_Click(object sender, RoutedEventArgs e)  
        {  
            geo = new Geolocator();  
            bool isErr = false;  
            string errMsg = "";  
  
            button1.IsEnabled = false;  
  
            try  
            {  
                Geoposition pos = await geo.GetGeopositionAsync();  
                double lat = pos.Coordinate.Point.Position.Latitude;  
                double lon = pos.Coordinate.Point.Position.Longitude;  
  
                lblLatitude.Text = lat.ToString();  
                lblLongitude.Text = lon.ToString();  
  
                var ap = ApplicationData.Current.LocalSettings;  
  
                CultureInfo cI = new CultureInfo(ap.Values["Language"].ToString());  
  
                String devName = new EasClientDeviceInformation().FriendlyName;  
  
                HttpClient hwc = new HttpClient();  
                Uri myAddress = new Uri(ap.Values["WebURI"].ToString() + 
                "?lt=" +lat.ToString(cI.NumberFormat) + "&ln=" + 
                lon.ToString(cI.NumberFormat) + "&d=" + 
                devName + "&n=" + txtNotes.Text);  
  
                HttpResponseMessage x = await hwc.GetAsync(myAddress);  
  
            }  
            catch (Exception ex)  
            {  
                isErr = true;  
                errMsg = ex.Message;  
            }  
  
            if (isErr)  
            {  
                var dialog = new MessageDialog(errMsg);  
                await dialog.ShowAsync();  
            }  
  
            button1.IsEnabled = true;  
            geo = null;  
        }  
  
        private void AppBarButton_Click(object sender, RoutedEventArgs e)  
        {  
            Frame.Navigate(typeof(Settings));  
        }  
  
    }  
}  

单击 button1 时,将创建 Geolocator 的一个新实例。从中,我们尝试异步检索设备位置,读取我们当前的经度/纬度,然后写入我们的 TextBlock。通过第 25 至 36 行,我们就完成了地理定位所需的一切,其余代码是关于访问我们的 Web 服务器的。首先,我们访问应用程序的 LocalSettings 以读取我们的 URILanguage 参数,并使用后者的值初始化 Culture。这将帮助我们确定如何将地理坐标传递给 Web 服务器(主要关于十进制分隔符,是逗号还是点)。

然后,使用 EasClientDeviceInformation 类,我们提取设备的 FriendlyName 或设备名称(第 44 行)。

接下来,我们使用 HttpClient 类打包 Web 请求,异步调用一个之前忘记的 URI(第 46 至 52 行),我们在其中将坐标、设备名称和任何备注作为 GET 参数给出,这正是我们的 PHP 页面所期望的。

如果发生错误,将显示一个 MessageDialog

重要提示:要使一切正常工作,必须检查两件基本事情。第一件是确保您的设备已激活位置服务。第二件是在应用本身中启用位置功能。在您的项目中,必须双击 *package.appxmanifest* 文件,然后选择“Capabilities”选项卡。然后在 Capabilities 列表中勾选 Location。否则,您的应用将无法为您进行地理定位。

就是这样,正如您所见,这是一个非常简单的应用,可以进行许多改进。但是,现有的应用足以满足我们的需求。

检查收集的数据

正如我们所见,当我们的应用发起 Web 请求时,将在远程数据库中插入一条新记录。现在是时候查询数据库来检查数据了。以下代码将实现一个简单的 HTML 页面,其中包含一个表,每条记录一行。它将显示记录插入的时间、坐标、设备名称、用户注解(如果有)。最后,每行都将以一个超链接结束,该超链接将导航到一个页面,我们可以在其中查看地图(以及在签到点上的图形标记)。我们将此页面命名为“*geolist.php*”。

<html>  
    <head>  
        <title>GeoLog</title>  
        <!-- OMITTED: CSS part, you'll find it in the complete source code -->  
    </head>  
  
    <body>  
        <table>  
            <tr>  
                <th>Id</th>  
                <th>Date</th>  
                <th>Lat.</th>  
                <th>Long.</th>  
                <th>Device</th>  
                <th>Annotations</th>  
                <th>Map</th>  
            </tr>  
  
        <?php  
  
        //-- Connecting to "geolog" database, on "localhost" server  
        mysqli = new mysqli('localhost', '<USER>', '<PASSWORD>', 'geolog');  
        if ($mysqli->connect_errno) {  
            echo "Failed to connect to MySQL: 
            (" . $mysqli->connect_errno . ") " . $mysqli->connect_error;  
            exit();  
        }  
  
        //-- Declaring and executing a SELECT on "entries" table, 
        //to retrieve all the records in it  
        $query = "SELECT * FROM entries ORDER BY IdEntry";  
        $result = $mysqli->query($query);  
  
        //-- For each retrieved record, we'll add to the DOM a table row, containing the read values  
        while($row = $result->fetch_array()){ ?>  
  
            <tr>  
                <td><?php echo $row['IdEntry'];?></td>  
                <td><?php echo $row['TimeStamp'];?></td>  
                <td><?php echo $row['Latitude'];?></td>  
                <td><?php echo $row['Longitude'];?></td>  
                <td><?php echo $row['Device'];?></td>  
                <td><?php echo $row['Annotation'];?></td>  
                <td>[<a href="map.php?lt=<?php echo $row['Latitude'];?>&
                ln=<?php echo $row['Longitude'];?>&
                d=<?php echo $row['Device'];?>&
                n=<?php echo $row['Annotation'];?>">Link</a>]</td>  
            </tr>  
        <?php }  
  
        //-- Close connection / cleanup  
        $result->close();  
        $mysqli->close();  
    ?>  
</body>  

忽略样式部分,我们所做的是连接到我们的 MySQL 数据库(即“geolog”),然后查询“entries”表。对于每个记录集,我们将创建一个表行及其子元素,六个单元格将显示读取的数据。请注意,我们的最终超链接指向一个名为“*map.php*”的页面。该页面是我们将在其中渲染相对于给定坐标的地图的页面。让我们看看页面效果。

<?php  
    $latitude = $_GET['lt'];  
    $longitude = $_GET['ln'];  
    $device = $_GET['d'];  
    $annotation = $_GET['n'];  
?>  
  
<html>  
    <head>  
        <title>GeoLog</title>  
        <meta name="viewport" content="initial-scale=1.0, user-scalable=no">  
        <meta charset="utf-8">  
        <style>  
            html, body, #map {  
                height: 100%;  
                margin: 0px;  
                padding: 0px;  
                width:100%;  
            }  
        </style>  
        <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&signed_in=true&language=it"></script>  
        <script>  
            var initialize = function(){  
            var latlng new google.maps.LatLng(<?php echo $latitude, ",", $longitude;?>);  
            var options = { zoom: 18,  
                center: latlng,  
                mapTypeId: google.maps.MapTypeId.ROADMAP  
            };  
            var map = new google.maps.Map(document.getElementById('map'), options);  
  
            var marker = new google.maps.Marker({ position: latlng,  
map: map,  
                title: '<?php echo $device;?>' });  
            }  
  
            window.onload = initialize;  
        </script>  
    </head>  
<body>  
<div id="map"></div>  
    </body>  
</html>  

为了避免重新查询数据库,我们将使用 GET 方法将所有所需信息传递给我们的 *map.php* 页面。该页面使用 JavaScript 和 Google Places API 的一些函数(开发人员参考在此)。主函数工作方式如下:它根据给定的纬度和经度创建一个地图,并将其分配给一个用于绘制的 DIV 元素。然后,创建一个 Marker 来在特定位置添加一个图形占位符。由于我们需要将 GET 参数用作 JavaScript 变量,因此我们将使用一个小技巧。JavaScript 在客户端执行,即在用户计算机上;PHP 在服务器端执行。因此,如果我们添加 PHP 指令,它们将在 JavaScript 部分之前执行,当页面“到达”用户设备时,它将被远程 PHP 执行修改。使用此方法,我们可以简单地将 GET 变量 echo 为 JavaScript 参数,这将写入将在客户端运行的代码中,从而允许查看正确的地图。

测试整个包

完成编码后,我们终于可以打包所有内容并进行整体测试了。首先,我们将在设备上打开我们的 WP 应用,并将其配置为指向我们设置的 URI。接下来,我们将单击“签到”按钮以启动地理定位和进一步的 Web 请求。在此阶段,我们的应用将构建要查询的 URI,然后访问它(从而触发远程 INSERT 过程)。

setting

checkin

现在,我们将打开浏览器访问 *geolist.php* 页面以检查插入的记录。我们将看到我们最近的签到,并且可以单击其链接打开 *map.php* 页面,该页面将以图形方式显示我们之前保存的位置。

map

源代码

本文使用的源代码可在此处下载:https://code.msdn.microsoft.com/Geolocalize-a-device-and-dae0d265

参考文献

历史

  • 2015-06-19:CodeProject 的第一个版本
© . All rights reserved.