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





5.00/5 (6投票s)
将设备地理定位并存储坐标到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
”的表。在这里,我们将存储智能手机发送的所有数据,即纬度/经度、设备名称(以防我们要跟踪多个设备,按名称选择每个设备)和用户注解。其余字段(IdEntry
和 TimeStamp
)将由列上的默认约束自动填充。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;
现在,在我们的 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 上。
您在地址栏中看到的 */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>
请注意,我添加了一个 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 TextBox
和 cmbLang 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
页面,这里没有其他特殊之处。唯一需要我们考虑代码隐藏的是页面上唯一 Button
的 Click
事件。
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
以读取我们的 URI
和 Language
参数,并使用后者的值初始化 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 过程)。
现在,我们将打开浏览器访问 *geolist.php* 页面以检查插入的记录。我们将看到我们最近的签到,并且可以单击其链接打开 *map.php* 页面,该页面将以图形方式显示我们之前保存的位置。
源代码
本文使用的源代码可在此处下载:https://code.msdn.microsoft.com/Geolocalize-a-device-and-dae0d265。
参考文献
历史
- 2015-06-19:CodeProject 的第一个版本