大家好,又见面了,我是你们的朋友全栈君。
mean shift应用
介绍 (Introduction)
Welcome back!
欢迎回来!
Last time, we created an application that integrated Google Maps directly into the MEAN stack. The app provided us a panel to create users, tag their location based on latitude and longitude, and validate their whereabouts using HTML5 geolocation.
上一次 ,我们创建了将Google Maps直接集成到MEAN堆栈中的应用程序。 该应用程序为我们提供了一个面板,用于创建用户,根据纬度和经度标记他们的位置,并使用HTML5地理位置验证他们的下落。
As of this writing, over 150 users have added themselves to our demo map, with diverse locations strewn from San Francisco to Melbourne — which is already pretty cool when you think about it!
截至撰写本文时,已有超过150位用户添加到我们的演示地图中 ,从旧金山到墨尔本遍布不同的地点-考虑到这已经非常酷!
Today, we’ll be taking our work a step further by adding a new control panel that allows us to filter users based on a variety of fields. The final product will allow us to query our map based on gender, age, favorite language, proximity, and whether a user’s location has been HTML5 verified. Additionally, this tutorial will give us an opportunity to introduce some of MongoDB’s geospatial query tools.
今天,我们将通过添加一个新的控制面板使我们的工作更进一步,该控制面板使我们能够根据各种字段过滤用户。 最终产品将使我们能够根据性别,年龄,喜欢的语言,邻近度以及用户的位置是否已通过HTML5验证来查询地图。 此外,本教程将使我们有机会介绍MongoDB的某些地理空间查询工具。
As you follow along, feel encouraged to grab the source code. Also, if you’re joining us for the first time, you can download the code from Part I using this link.
在继续学习时,鼓励您抓住源代码 。 另外,如果您是第一次加入我们,则可以使用此链接从第I部分下载代码。
修改后的应用程序骨架 (Revised App Skeleton)
To begin, let’s make some adjustments to our app’s structure. Go ahead and create a new queryCtrl.js
file as well as a directory called partials
, which will hold the files addForm.html
and queryForm.html
.
首先,让我们对应用程序的结构进行一些调整。 继续创建一个新的queryCtrl.js
文件以及一个名为partials
的目录,该目录将包含文件addForm.html
和queryForm.html
。
MapApp
-- app // BACKEND
---- model.js
---- routes.js
-- public // FRONTEND
---- index.html
---- js
------ app.js
------ addCtrl.js
------ queryCtrl.js // *new*
------ gservice.js
---- style.css
---- partials // *new*
------ addForm.html // *new*
------ queryForm.html // *new*
-- server.js // EXPRESS SERVER
-- package.json
创建查询视图 (Creating the Query View)
Since our app wil now have two separate control panels — one for adding users and one for querying users, we’re going to utilize Angular’s routing module ngRoute to display the correct panel when needed.
由于我们的应用程序现在将具有两个单独的控制面板-一个用于添加用户,一个用于查询用户,因此我们将在需要时利用Angular的路由模块ngRoute来显示正确的面板。
To do this, we’re going to store the code associated with each panel in its own HTML partial. We’ll then specify in our main Angular module (app.js
) that our application should display the queryForm
partial when the URL includes /find
and the addForm
partial for all other URLs.
为此,我们将与每个面板关联的代码存储在其自己HTML部分中。 然后,我们将在我们的主角度模块(指定app.js
),我们的应用程序应该显示queryForm
部分当URL包含/find
和addForm
部分的所有其他网址。
Let’s go ahead and extract the ‘Add Form’ code previously found in our index.html
file and paste it into the addForm.html
file of our partials
folder.
让我们继续提取先前在index.html
文件中找到的“添加表单”代码,并将其粘贴到partials
文件夹的addForm.html
文件中。
<!-- addForm.html -->
<!-- "Join Team" (Post) Form -->
<div class="col-md-5">
<!-- Creates Main Panel -->
<div class="panel panel-default">
<!-- Panel Title -->
<div class="panel-heading">
<h2 class="panel-title text-center">Join the Scotch Team! <span class="glyphicon glyphicon-map-marker"></span></h2>
</div>
<!-- Panel Body -->
<div class="panel-body">
<!-- Creates Form (novalidate disables HTML validation, Angular will control) -->
<form name ="addForm" novalidate>
<!-- Text Boxes and Other User Inputs. Note ng-model binds the values to Angular $scope -->
<div class="form-group">
<label for="username">Username <span class="badge">All fields required</span></label>
<input type="text" class="form-control" id="username" placeholder="OldandGold" ng-model="formData.username" required>
</div>
<label class="radio control-label">Gender</label>
<div class="radio">
<label>
<input type="radio" name="optionsRadios" id="radiomale" value="Male" ng-model="formData.gender">
Male
</label>
</div>
<div class="radio" required>
<label>
<input type="radio" name="optionsRadios" id="radiofemale" value="Female" ng-model="formData.gender">
Female
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="optionsRadios" id="radioother" value="What's it to ya?" ng-model="formData.gender">
What's it to ya?
</label>
</div>
<div class="form-group">
<label for="age">Age</label>
<input type="number" class="form-control" id="age" placeholder="72" ng-model="formData.age" required>
</div>
<div class="form-group">
<label for="language">Favorite Language</label>
<input type="text" class="form-control" id="language" placeholder="Fortran" ng-model="formData.favlang" required>
</div>
<div class="form-group">
<label for="latitude">Latitude</label>
<input type="text" class="form-control" id="latitude" value="39.500" ng-model="formData.latitude" readonly>
</div>
<div class="form-group">
<label for="longitude">Longitude</label>
<input type="text" class="form-control" id="longitude" value="-98.350" ng-model="formData.longitude" readonly>
</div>
<div class="form-group">
<!-- Note RefreshLoc button tied to addCtrl. This requests a refresh of the HTML5 verified location. -->
<label for="verified">HTML5 Verified Location? <span><button ng-click="refreshLoc()" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-refresh"></span></button></span></label>
<input type="text" class="form-control" id="verified" placeholder= "Nope (Thanks for spamming my map...)" ng-model="formData.htmlverified" readonly>
</div>
<!-- Submit button. Note that its tied to createUser() function from addCtrl. Also note ng-disabled logic which prevents early submits. -->
<button type="submit" class="btn btn-danger btn-block" ng-click="createUser()" ng-disabled="addForm.$invalid">Submit</button>
</form>
</div>
</div>
</div>
Next, let’s paste the code associated with our new Query Form into the queryForm.html
of the same folder.
接下来,让我们将与新查询表单关联的代码粘贴到同一文件夹的queryForm.html
中。
<!-- queryForm.html -->
<!-- Find Teammates (Query) Form -->
<div class="col-md-5">
<!-- Creates Main Panel -->
<div class="panel panel-default">
<!-- Panel Title -->
<div class="panel-heading">
<h2 class="panel-title text-center">Find Teammates! (Map Query) <span class="glyphicon glyphicon-search"></span></h2>
</div>
<!-- Panel Body -->
<div class="panel-body">
<!-- Creates Form -->
<form name ="queryForm">
<!-- Text Boxes and Other User Inputs. Note ng-model binds the values to Angular $scope -->
<div class="form-group">
<label for="latitude">Your Latitude</label>
<input type="text" class="form-control" id="latitude" placeholder="39.5" ng-model="formData.latitude" readonly>
</div>
<div class="form-group">
<label for="longitude">Your Longitude</label>
<input type="text" class="form-control" id="longitude" placeholder="-98.35" ng-model="formData.longitude" readonly>
</div>
<div class="form-group">
<label for="distance">Max. Distance (miles)</label>
<input type="text" class="form-control" id="distance" placeholder="500" ng-model="formData.distance">
</div>
<!-- Note ng-true-value which translates check values into explicit gender strings -->
<label>Gender</label>
<div class="form-group">
<label class="checkbox-inline">
<input type="checkbox" name="optionsRadios" id="checkmale" value="Male" ng-model="formData.male" ng-true-value = "'Male'">
Male
</label>
<label class="checkbox-inline">
<input type="checkbox" name="optionsRadios" id="checkfemale" value="Female" ng-model="formData.female" ng-true-value="'Female'">
Female
</label>
<label class="checkbox-inline">
<input type="checkbox" name="optionsRadios" id="checkother" value="What's it to ya?" ng-model="formData.other" ng-true-value="'What\'s it to ya?'">
What's it to ya?
</label>
</div>
<div class="form-group">
<label for="minage">Min. Age</label>
<input type="number" class="form-control" id="minage" placeholder="5" ng-model="formData.minage">
</div>
<div class="form-group">
<label for="maxage">Max Age</label>
<input type="number" class="form-control" id="maxage" placeholder="80" ng-model="formData.maxage">
</div>
<div class="form-group">
<label for="favlang">Favorite Language</label>
<input type="text" class="form-control" id="favlang" placeholder="Fortran" ng-model="formData.favlang">
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" name="verified" id="radiomale" value="True" ng-model="formData.verified"> <strong>Include Only HTML5 Verified Locations?</strong>
</label>
</div>
</div>
<!-- Query button. Note that its tied to queryUsers() function from queryCtrl. -->
<button type="submit" class="btn btn-danger btn-block" ng-click="queryUsers()">Search</button>
</form>
</div>
<!-- Footer panel for displaying count. Note how it will only display if queryCount is greater than 0 -->
<div ng-show="queryCount>0" class="panel-footer">
<p class="text-center">Hot Dang! We Found {
{queryCount}} Teammates.</p>
</div>
</div>
</div>
Lastly, let’s update our meanMapApp
module in the app.js
file to include the Angular ngRoute
module and specify the templateURL
associated with each URL route.
最后,让我们更新meanMapApp
模块中的app.js
文件以包括角ngRoute
模块,并指定templateURL
每个URL路由关联。
// app.js
// Declares the initial angular module "meanMapApp". Module grabs other controllers and services. Note the use of ngRoute.
var app = angular.module('meanMapApp', ['addCtrl', 'geolocation', 'gservice', 'ngRoute'])
// Configures Angular routing -- showing the relevant view and controller when needed.
.config(function($routeProvider){
// Join Team Control Panel
$routeProvider.when('/join', {
controller: 'addCtrl',
templateUrl: 'partials/addForm.html',
// Find Teammates Control Panel
}).when('/find', {
templateUrl: 'partials/queryForm.html',
// All else forward to the Join Team Control Panel
}).otherwise({redirectTo:'/join'})
});
For those less familiar with Angular’s ngRoute
module, what we’ve done here is made use of Angular’s routeProvider
service to identify the URL our users are looking at in the browser. Thus, when a user is looking at a URL with a given suffix, Angular knows which pre-defined controller and templateURL to use.
对于那些不太熟悉Angular的ngRoute
模块的人,我们在这里所做的就是利用Angular的routeProvider
服务来标识用户在浏览器中正在查看的URL。 因此,当用户查看带有给定后缀的URL时,Angular知道要使用哪个预定义的控制器和templateURL。
As you can see, in the example above, when a user is looking at /join
(or any URL other than /find
), Angular will employ the addCtrl
controller that we created in Part I and display the content from our addForm.html
file. Similarly when a user is looking at /find
, the user will be displayed the queryForm.html
content. (Once we create the queryCtrl
controller, we will specify this here as well.)
如您所见,在上面的示例中,当用户查看/join
(或/find
以外的任何URL)时,Angular将使用我们在第一部分中创建的addCtrl
控制器并显示addForm.html
文件中的内容。 。 同样,当用户查看/find
,将向用户显示queryForm.html
内容。 (一旦创建了queryCtrl
控制器,我们也将在此处指定它。)
Now that we have our partials ready, let’s update our index.html
file.
现在我们已经准备好了部分文件,让我们更新index.html
文件。
<!-- index.html -->
<!doctype html>
<!-- Declares meanMapApp as the starting Angular module -->
<html class="no-js" ng-app="meanMapApp">
<head>
<meta charset="utf-8">
<title>Scotch MEAN Map</title>
<meta name="description" content="An example demonstrating Google Map integration with MEAN Apps">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSS -->
<link rel="stylesheet" href="../bower_components/bootstrap/dist/css/bootstrap.css"/>
<link rel="stylesheet" href="style.css"/>
<!-- Google Maps API -->
<script src="https://maps.proxy.ustclug.org/maps/api/js?key=AIzaSyDrn605l7RPadiwdzsOlRw9O28lxfYBJ6s"></script>
<!-- Modernizr -->
<script src="../bower_components/modernizr/bin/modernizr"></script>
<!-- JS Source -->
<script src="../bower_components/jquery/jquery.js"></script>
<script src="../bower_components/angular/angular.js"></script>
<script src="../bower_components/angular-route/angular-route.js"></script>
<script src="../bower_components/angularjs-geolocation/dist/angularjs-geolocation.min.js"></script>
<!-- Angular Source -->
<script src="js/app.js"></script>
<script src="js/addCtrl.js"></script>
<script src="js/gservice.js"></script>
</head>
<!-- Removed ng-controller. Now this will be handled in app.js -->
<body>
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<!-- Links to the two menu views included -->
<li active><a href="/#/join">Join the Team</a></li>
<li disabled><a href="/#/find">Find Teammates</a></li>
</ul>
<h3 class="text-muted">The Scotch MEAN MapApp</h3>
</div>
<!-- Map and Side Panel -->
<div class="row content">
<!-- Google Map -->
<div class="col-md-7">
<div id="map"></div>
</div>
<!-- Side Panel -- Now Handled by ng-view -->
<div ng-view></div>
</div>
<hr/>
<!-- Footer -->
<div class="footer">
<p class="text-center"><span class="glyphicon glyphicon-check"></span> Created by Ahmed Haque for Scotch IO -
<a href="https://scotch.io/">App Tutorial</a> | <a href="https://github.com/afhaque/MeanMapAppV2.0">Github Repo</a></p>
</div>
</div>
</body>
</html>
Here we’ve removed the content associated with our addForm
side-panel and replaced it with a generic reference to ng-view
. Angular’s routeProvider
will automatically replace this with the correct HTML partial.
在这里,我们删除了与addForm
侧面板相关联的内容,并将其替换为对ng-view
的通用引用。 Angular的routeProvider
会自动将其替换为正确HTML部分。
Additionally, we’ve removed reference to the addCtrl
controller that previously existed in our body tag. Once again, our routeprovider
will instruct the page to use the correct controller based on the URL.
此外,我们还删除了对先前在body标签中存在的addCtrl
控制器的引用。 再次,我们的routeprovider
将指示页面根据URL使用正确的控制器。
With that, its time for our first test!
这样,就可以进行首次测试了!
Crank up your mongod
instance and run node server.js
. If you then head to localhost:3000
you should see our familiar map from Part I. Click the ‘Find Teammates’ link to see our new Query Form basking in all its glory.
启动您的mongod
实例并运行node server.js
。 如果您随后转到localhost:3000
,则应该会看到第一部分中我们熟悉的地图。单击“查找队友”链接,以查看我们全新的查询表单的所有荣耀。
创建查询逻辑和快速路由 (Creating the Query Logic and Express Routes)
Now its time to create the backend code for handling queries. Open your routes.js
file and paste the following code beneath your app.post('users')
code.
现在该创建用于处理查询的后端代码了。 打开您的routes.js
文件,并将以下代码粘贴到您的app.post('users')
代码下方。
// routes.js
// app.post('users') code for creating users...
// ...
// Retrieves JSON records for all users who meet a certain set of query conditions
app.post('/query/', function(req, res){
// Grab all of the query parameters from the body.
var lat = req.body.latitude;
var long = req.body.longitude;
var distance = req.body.distance;
// Opens a generic Mongoose Query. Depending on the post body we will...
var query = User.find({});
// ...include filter by Max Distance (converting miles to meters)
if(distance){
// Using MongoDB's geospatial querying features. (Note how coordinates are set [long, lat]
query = query.where('location').near({ center: {type: 'Point', coordinates: [long, lat]},
// Converting meters to miles. Specifying spherical geometry (for globe)
maxDistance: distance * 1609.34, spherical: true});
}
// ... Other queries will go here ...
// Execute Query and Return the Query Results
query.exec(function(err, users){
if(err)
res.send(err);
// If no errors, respond with a JSON of all users that meet the criteria
res.json(users);
});
});
We’ve done a couple of key things here. So let’s break it down:
我们在这里做了一些关键的事情。 因此,让我们分解一下:
-
First, we created a new
POST
request handler for URLs with the suffix/query
. This handler expects a JSON request body, which specifies three parameters: latitude, longitude, and distance. These parameters are then converted and stored as variables in the handler.首先,我们为后缀为
/query
URL创建了一个新的POST
请求处理程序。 该处理程序需要一个JSON请求主体,该主体指定三个参数:纬度,经度和距离。 然后将这些参数转换并作为变量存储在处理程序中。 -
We then created a generic Mongoose Query using the Query Builder format. This format begins by establishing a generic
query
object equal to the unfiltered search of all users in our database.然后,我们使用查询生成器格式创建了一个通用的猫鼬查询。 这种格式首先建立一个通用
query
对象,该对象等于数据库中所有用户的未过滤搜索。 -
If a distance is provided, the Query Builder will add a new search condition that filters for all users that fall within the distance provided of the query’s coordinates (latitude, longitude). Here we’re using the MongoDB search parameter $near and its associated properties
maxDistance
andspherical
to specify the range we’re looking to cover. We’re multiplying the distance of our query body by 1609.34, because we want to take our users’ input (in miles) and convert it into the units MongoDB expects (in meters). Lastly, we’re specifying that the distance should be determined assuming a spherical surface. This is important, because we’ll be evaluating distances across the globe, as opposed to a flat Euclidean surface.如果提供了距离,则查询生成器将添加一个新的搜索条件,该条件将对位于查询坐标提供的距离(纬度,经度)内的所有用户进行过滤。 在这里,我们使用MongoDB搜索参数$ near及其关联的属性
maxDistance
和spherical
来指定我们要覆盖的范围。 我们将查询正文的距离乘以1609.34,因为我们要获取用户的输入(以英里为单位)并将其转换为MongoDB期望的单位(以米为单位)。 最后,我们指定应在假设球形表面的情况下确定距离。 这很重要,因为我们将评估地球上的距离,而不是平坦的欧几里得曲面。 -
Finally, we used
query.exec
to instruct Mongoose to run the final query. If the query encounters no errors, it will provide a JSON output of all users who meet the criteria.最后,我们使用
query.exec
指示Mongoose运行最终查询。 如果查询没有错误,它将提供所有符合条件的用户的JSON输出。
At this point, let’s test what we have. To do this, re-run your application using node server.js
and place a few dummy markers on your map. Place two markers near each other on one side of the map and two markers a sizeable distance away. Then position your marker next to the first two markers and note the associated latitude and longitude.
在这一点上,让我们测试一下我们所拥有的。 为此,请使用node server.js
重新运行您的应用程序,然后在地图上放置一些虚拟标记。 将两个标记彼此靠近放置在地图的一侧,两个标记相距一定距离。 然后将您的标记放置在前两个标记旁边,并注意相关的纬度和经度。
Now, open up Postman and create a raw JSON POST
request to your /query
URL. Specify the latitude and longitude that you just noted and set a distance of 100. Then send the request.
现在,打开Postman并为/query
URL创建一个原始的JSON POST
请求。 指定刚刚记下的纬度和经度,并将距离设置为100。然后发送请求。
If all went well, your response body should list only the new nearby markers and exclude the distant ones.
如果一切顺利,您的响应正文应仅列出附近的新标记,并排除较远的标记。
Repeat your POST
request, but set the distance much farther. Try 1000 or 5000 instead.
重复您的POST
请求,但将距离设置得更远。 尝试使用1000或5000。
If all went well, your response should list the remaining markers as well.
如果一切顺利,您的回复也应列出其余标记。
We’ll examine the precision capabilities of our query a bit later, but for now, let’s add the remaining filter conditions.
稍后我们将检查查询的精度功能,但现在,让我们添加其余的过滤条件。
To do this, paste the following code over the POST request we just created.
为此,请将以下代码粘贴到我们刚刚创建的POST请求上。
// routes.js
// Retrieves JSON records for all users who meet a certain set of query conditions
app.post('/query/', function(req, res){
// Grab all of the query parameters from the body.
var lat = req.body.latitude;
var long = req.body.longitude;
var distance = req.body.distance;
var male = req.body.male;
var female = req.body.female;
var other = req.body.other;
var minAge = req.body.minAge;
var maxAge = req.body.maxAge;
var favLang = req.body.favlang;
var reqVerified = req.body.reqVerified;
// Opens a generic Mongoose Query. Depending on the post body we will...
var query = User.find({});
// ...include filter by Max Distance (converting miles to meters)
if(distance){
// Using MongoDB's geospatial querying features. (Note how coordinates are set [long, lat]
query = query.where('location').near({ center: {type: 'Point', coordinates: [long, lat]},
// Converting meters to miles. Specifying spherical geometry (for globe)
maxDistance: distance * 1609.34, spherical: true});
}
// ...include filter by Gender (all options)
if(male || female || other){
query.or([{ 'gender': male }, { 'gender': female }, {'gender': other}]);
}
// ...include filter by Min Age
if(minAge){
query = query.where('age').gte(minAge);
}
// ...include filter by Max Age
if(maxAge){
query = query.where('age').lte(maxAge);
}
// ...include filter by Favorite Language
if(favLang){
query = query.where('favlang').equals(favLang);
}
// ...include filter for HTML5 Verified Locations
if(reqVerified){
query = query.where('htmlverified').equals("Yep (Thanks for giving us real data!)");
}
// Execute Query and Return the Query Results
query.exec(function(err, users){
if(err)
res.send(err);
// If no errors, respond with a JSON of all users that meet the criteria
res.json(users);
});
});
What we’ve done here is successively added conditions that check if our user has provided distance, gender, age, language, or HTML5 verified constraints to the POST
body. If any of these constraints exist, we’ll add the associated query condition to our Query Builder. Take note of this example as it really highlights the value of Mongoose’s Query Builder for complex queries.
我们在这里所做的是连续添加条件,以检查我们的用户是否向POST
正文提供了距离,性别,年龄,语言或经过HTML5验证的约束。 如果存在任何这些约束,则将关联的查询条件添加到查询生成器中。 注意这个例子,因为它确实突出了Mongoose的Query Builder对于复杂查询的价值。
Speaking of complex queries, let’s go ahead and test one now. To do this, create a set of mock users in various locations with assorted characteristics.
说到复杂的查询,让我们继续测试一下。 为此,请在各个位置创建具有各种特征的一组模拟用户。
Here I’ve created a set of markers around Indianapolis.
在这里,我在印第安纳波利斯附近创建了一组标记。
Let’s say, I’m creating a coding school that targets girls between the ages of 20-30 years of age, within the city limits (150 miles). I can convert these parameters into POST
request fields as shown below.
假设,我正在创建一所编码学校,针对城市限制(150英里)内20至30岁之间的女孩。 我可以将这些参数转换为POST
请求字段,如下所示。
Then when I run the query, I see only the filtered set of results.
然后,当我运行查询时,我只会看到过滤后的结果集。
Huzzah! IndyCodingSchool here we come.
晕! IndyCodingSchool我们来了。
创建查询控制器 (Creating the Query Controller)
Okay. That was great, but writing JSON requests manually can seriously suck. We need to build our UI capabilities ASAP!
好的。 很好,但是手动编写JSON请求可能会严重失败。 我们需要尽快构建我们的UI功能!
To begin, let’s paste the following code in our queryCtrl.js
file.
首先,让我们将以下代码粘贴到我们的queryCtrl.js
文件中。
// queryCtrl.js
// Creates the addCtrl Module and Controller. Note that it depends on 'geolocation' and 'gservice' modules.
var queryCtrl = angular.module('queryCtrl', ['geolocation', 'gservice']);
queryCtrl.controller('queryCtrl', function($scope, $log, $http, $rootScope, geolocation, gservice){
// Initializes Variables
// ----------------------------------------------------------------------------
$scope.formData = {};
var queryBody = {};
// Functions
// ----------------------------------------------------------------------------
// Get User's actual coordinates based on HTML5 at window load
geolocation.getLocation().then(function(data){
coords = {lat:data.coords.latitude, long:data.coords.longitude};
// Set the latitude and longitude equal to the HTML5 coordinates
$scope.formData.longitude = parseFloat(coords.long).toFixed(3);
$scope.formData.latitude = parseFloat(coords.lat).toFixed(3);
});
// Get coordinates based on mouse click. When a click event is detected....
$rootScope.$on("clicked", function(){
// Run the gservice functions associated with identifying coordinates
$scope.$apply(function(){
$scope.formData.latitude = parseFloat(gservice.clickLat).toFixed(3);
$scope.formData.longitude = parseFloat(gservice.clickLong).toFixed(3);
});
});
// Take query parameters and incorporate into a JSON queryBody
$scope.queryUsers = function(){
// Assemble Query Body
queryBody = {
longitude: parseFloat($scope.formData.longitude),
latitude: parseFloat($scope.formData.latitude),
distance: parseFloat($scope.formData.distance),
male: $scope.formData.male,
female: $scope.formData.female,
other: $scope.formData.other,
minAge: $scope.formData.minage,
maxAge: $scope.formData.maxage,
favlang: $scope.formData.favlang,
reqVerified: $scope.formData.verified
};
// Post the queryBody to the /query POST route to retrieve the filtered results
$http.post('/query', queryBody)
// Store the filtered results in queryResults
.success(function(queryResults){
// Query Body and Result Logging
console.log("QueryBody:");
console.log(queryBody);
console.log("QueryResults:");
console.log(queryResults);
// Count the number of records retrieved for the panel-footer
$scope.queryCount = queryResults.length;
})
.error(function(queryResults){
console.log('Error ' + queryResults);
})
};
});
What we’ve done here is very similar to the work we did in Part I. We created a new module and controller called queryCtrl
. This controller relies on $scope
to pull all of the form data from our active queryForm.html
file. These elements are converted into variables, which are then used to directly create an http POST
request to the /query
URL whenever the $scope.queryUsers
function (associated with our query button) is triggered. Additionally, as was the case with our addCtrl
controller, the queryCtrl
has code for identifying a user’s current location and for handling click capture.
我们在这里所做的工作与在第一部分中所做的工作非常相似。我们创建了一个名为queryCtrl
的新模块和控制器。 该控制器依靠$scope
从活动的queryForm.html
文件中提取所有表单数据。 这些元素被转换为变量,每当触发$scope.queryUsers
函数(与我们的查询按钮关联)时,这些变量就可用于直接向/query
URL创建一个HTTP POST
请求。 另外,与我们的addCtrl
控制器一样, queryCtrl
具有用于标识用户当前位置和用于处理点击捕获的代码。
Now that our controller is ready, let’s add a reference to queryCtrl
in our main Angular module in app.js
.
现在我们的控制器已经准备就绪,让我们在app.js
主要Angular模块中添加对queryCtrl
的引用。
// app.js
var app = angular.module('meanMapApp', ['addCtrl', 'queryCtrl', 'geolocation', 'gservice', 'ngRoute'])
We’ll also update our $routeProvider
to utilize this new controller when a user is looking at the /find
URL.
当用户查看/find
URL时,我们还将更新$routeProvider
以使用此新控制器。
// app.js
// Find Teammates Control Panel
}).when('/find', {
controller: 'queryCtrl',
templateUrl: 'partials/queryForm.html',
Finally, we’ll include a link to the queryCtrl.js
script in our index.html
file.
最后,我们将在我们的index.html
文件中包含一个指向queryCtrl.js
脚本的链接。
<script src="js/queryCtrl.js"></script>
Now that we’ve completed everything, let’s repeat the example from before. But this time, use the form itself to conduct the search.
现在我们已经完成了所有的工作,让我们从前面的例子开始重复。 但是这一次,使用表单本身进行搜索。
Since we haven’t updated our map service, we won’t see changes on the map just yet. However, if we open up our Google Developers Console (ctrl+shift+i
) and navigate to the console, we should see both our queryBody
and the queryResults
displayed.
由于我们尚未更新地图服务,因此我们暂时不会在地图上看到更改。 但是,如果我们打开Google Developers Console( ctrl+shift+i
)并导航到该控制台,则应该同时显示我们的queryBody
和queryResults
。
If all went well, the query results should match the results you saw earlier.
如果一切顺利,查询结果应与您之前看到的结果匹配。
Aha. Found them again!
啊哈 再次找到他们!
修改Google Maps Service (Modifying the Google Maps Service)
Now that we’re successfully filtering users, its time to visualize our results on the map itself. To do this, we’re going to make a series of modifications to our googleMapService found in gservice.js
.
现在,我们已经成功过滤了用户,现在可以在地图本身上可视化我们的结果了。 为此,我们将对gservice.js中的gservice.js
进行一系列修改。
Go ahead and paste the following code over our pre-existing refresh googleMapService.refresh
function.
继续,将以下代码粘贴到我们预先存在的刷新googleMapService.refresh
函数上。
// gservice.js
// Refresh the Map with new data. Takes three parameters (lat, long, and filtering results)
googleMapService.refresh = function(latitude, longitude, filteredResults){
// Clears the holding array of locations
locations = [];
// Set the selected lat and long equal to the ones provided on the refresh() call
selectedLat = latitude;
selectedLong = longitude;
// If filtered results are provided in the refresh() call...
if (filteredResults){
// Then convert the filtered results into map points.
locations = convertToMapPoints(filteredResults);
// Then, initialize the map -- noting that a filter was used (to mark icons yellow)
initialize(latitude, longitude, true);
}
// If no filter is provided in the refresh() call...
else {
// Perform an AJAX call to get all of the records in the db.
$http.get('/users').success(function(response){
// Then convert the results into map points
locations = convertToMapPoints(response);
// Then initialize the map -- noting that no filter was used.
initialize(latitude, longitude, false);
}).error(function(){});
}
};
Here what we’ve done is introduced a new optional parameter filteredResults
to the refresh
function.
在此,我们为refresh
功能引入了新的可选参数filteredResults
。
As you may recall, from Part I, the original purpose of the refresh
function was to pull information on all users in our database through a GET
request to /users
and to convert this data into Google Map markers. These markers were then used to populate our map, which was then displayed to users with their own location marked as well.
您可能还记得,在第一部分中, refresh
功能的初衷是通过对/users
的GET
请求提取数据库中所有用户的信息,并将此数据转换为Google Map标记。 这些标记随后被用来填充我们的地图,然后向用户显示带有自己位置的标记。
By adding in the filteredResults
parameter, we’re adapting the refresh
function for a second purpose. In cases, where we want to show only filtered results, we’re going to circumvent the $http
GET
request, and instead directly send a JSON that includes only the filter-limited results. We’ll be able to generate these results using the POST
request to the /query
route that we just created.
通过添加filteredResults
参数,我们将refresh
功能用于第二个目的。 在某些情况下,我们只想显示过滤后的结果,我们将绕过$http
GET
请求,而是直接发送仅包含过滤器受限结果的JSON。 我们将能够使用对刚创建的/query
路由的POST
请求生成这些结果。
Once the refresh
function receives these filtered results, it will pass the JSON to our convertToMapPoints
function and store the converted set of Google Map markers in our locations
array. We can then initialize our map as before, but this time only the filtered results will be shown.
refresh
函数收到这些过滤后的结果后,它将JSON传递给我们的convertToMapPoints
函数,并将转换后的Google Map标记集存储在我们的locations
数组中。 然后,我们可以像以前一样初始化地图,但是这次仅显示过滤后的结果。
Next, let’s make one more change to make things more obvious. Go ahead and paste the below code over the initialize
function.
接下来,让我们进行更多更改以使事情变得更加明显。 继续,将下面的代码粘贴到initialize
函数上。
// gservice.js
// Initializes the map
var initialize = function(latitude, longitude, filter) {
// Uses the selected lat, long as starting point
var myLatLng = {lat: selectedLat, lng: selectedLong};
// If map has not been created...
if (!map){
// Create a new map and place in the index.html page
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 3,
center: myLatLng
});
}
// If a filter was used set the icons yellow, otherwise blue
if(filter){
icon = "http://maps.google.com/mapfiles/ms/icons/yellow-dot.png";
}
else{
icon = "http://maps.google.com/mapfiles/ms/icons/blue-dot.png";
}
// Loop through each location in the array and place a marker
locations.forEach(function(n, i){
var marker = new google.maps.Marker({
position: n.latlon,
map: map,
title: "Big Map",
icon: icon,
});
// For each marker created, add a listener that checks for clicks
google.maps.event.addListener(marker, 'click', function(e){
// When clicked, open the selected marker's message
currentSelectedMarker = n;
n.message.open(map, marker);
});
});
// Set initial location as a bouncing red marker
var initialLocation = new google.maps.LatLng(latitude, longitude);
var marker = new google.maps.Marker({
position: initialLocation,
animation: google.maps.Animation.BOUNCE,
map: map,
icon: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png'
});
lastMarker = marker;
// Function for moving to a selected location
map.panTo(new google.maps.LatLng(latitude, longitude));
// Clicking on the Map moves the bouncing red marker
google.maps.event.addListener(map, 'click', function(e){
var marker = new google.maps.Marker({
position: e.latLng,
animation: google.maps.Animation.BOUNCE,
map: map,
icon: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png'
});
// When a new spot is selected, delete the old red bouncing marker
if(lastMarker){
lastMarker.setMap(null);
}
// Create a new red bouncing marker and move to it
lastMarker = marker;
map.panTo(marker.position);
// Update Broadcasted Variable (lets the panels know to change their lat, long values)
googleMapService.clickLat = marker.getPosition().lat();
googleMapService.clickLong = marker.getPosition().lng();
$rootScope.$broadcast("clicked");
});
};
The change here is minimal but visually significant. Here we’ve noted that if the initialize
function is called with the boolean filter
set to true, all markers should be yellow dots as opposed to the blue dots we’d been using before. This is helpful from a UI perspective, because it let’s users immediately realize that their query worked successfully.
此处的更改很小,但在视觉上很有意义。 在这里,我们已经注意到,如果在将布尔filter
设置为true的情况下调用了initialize
函数,则所有标记应为黄点,而不是我们之前使用的蓝点。 从UI角度看,这很有用,因为它使用户立即意识到他们的查询成功完成。
Now that our refresh
function has been updated. Let’s finally update our queryCtrl
file to utilize this new service. Add the below line in place of the console.log
lines we used earlier.
现在,我们的refresh
功能已更新。 最后,让我们更新queryCtrl
文件以利用此新服务。 添加以下行代替我们之前使用的console.log
行。
// queryCtrl.js
// $http.post('/query, queryBody).successs(function(queryResults){...
// Old console.log code ...
// Pass the filtered results to the Google Map Service and refresh the map
gservice.refresh(queryBody.latitude, queryBody.longitude, queryResults);
Here you can see that we’re taking the queryResults from our /query
POST
and directly sending the filtered results to our refresh
function for map building. And with this final step, let’s run one final test of our app!
在这里,您可以看到我们正在从/query
POST
获取queryResults并将过滤后的结果直接发送到我们的refresh
函数以进行地图构建。 最后一步,我们将对我们的应用程序进行最后的测试!
Boot up your server.js
file and rerun the advanced query example we’ve run before using the Query Form. If all went well you should see prominent yellow markers, indicating the filtered results.
启动您的server.js
文件,然后重新运行我们使用查询表单之前运行的高级查询示例。 如果一切顺利,您应该会看到显眼的黄色标记,表明已过滤的结果。
Victory. I see you!
胜利。 我看见你!
最终调整 (Final Tweaks)
There are plenty of ways to improve this app. As a few suggestions, I’d suggest reading up on GeoJSON, the various Google Map Options, and the different styling options available from Snazzy Maps. Additionally, there is significant momentum behind the Angular-Google-Maps Project, something definitely worth looking into if you’re planning to build more complex map applications.
有很多方法可以改善此应用程序。 作为一些建议,我建议您阅读GeoJSON ,各种Google Map Options和Snazzy Maps提供的各种样式选项。 此外, Angular-Google-Maps项目背后还有巨大的发展动力,如果您打算构建更复杂的地图应用程序,那么这绝对值得研究。
That said, even with as simple a map application as this one — it’s remarkable how quickly you can draw meaningful information. Already our demo app has had close to 100 verified users sign up. Scanning through the locations, it’s been awesome to see the diversity of locations that Scotch readers login from. (Who knew Scotch had a follower in Bishkek, Kyrgzstan?).
就是说,即使像这样一个简单的地图应用程序,也能以多快的速度绘制出有意义的信息。 我们的演示应用程序已经有近100位经过验证的用户注册。 扫描各个位置,看到苏格兰读者从中登录的位置非常棒。 (谁知道苏格兰威士忌在吉尔吉斯斯坦的比什凯克有追随者?)。
Good luck with your own map making adventures! If you come up with something cool, definitely post about it in the comments.
祝您自己制作地图冒险! 如果您提出了一些很酷的建议,请在评论中明确发表。
We’d love to hear about it!
我们希望知道这一点!
BONUS:“但是,这近Mongo的东西到底有多精确……?” (BONUS: “But just how accurate is this Mongo $near thing…?”)
Well. It turns out pretty damn accurate is the answer. While writing this tutorial, I’d stumbled into a few articles that anecdotally estimated that the $near
function was good enough at discriminating distances within 5 miles — so I decided to test and see for myself. I ran a few experiments with coordinates a known distance away from each other, and looked for the minimum distance I could use to discriminate locations.
好。 事实证明,答案很准确。 在编写本教程时,我偶然发现了几篇文章,据传闻$near
函数足以区分5英里以内的距离-因此,我决定亲自测试一下。 我进行了一些实验,它们的坐标彼此之间的距离已知,并且寻找可以用来区分位置的最小距离。
For small distances (where Euclidean “flat” geometry takes hold), Mongo distance queries were right on the money — consistently able to discriminate distances within 1 mile of the actual distance.
对于较小的距离(采用欧几里德“平坦”几何形状的地方),Mongo距离查询就很合适-始终能够区分实际距离1英里以内的距离。
For larger distances (where more complex “spherical” geometries become relevant), the MongoDB distance queries were off by about ~10 miles. Not as good, but not shabby at all — especially, considering there exist multiple methods for calculating spherical distances.
对于更大的距离(需要使用更复杂的“球形”几何形状),MongoDB距离查询减少了大约10英里。 不太好,但一点也不破旧-尤其是考虑到存在多种计算球面距离的方法。
All in all, this presents even more reason to play around with the Geospatial tools in MongoDB — so go forth with confidence young cartographers!
总而言之,这为在MongoDB中使用地理空间工具提供了更多的理由-因此,年轻的制图师应该放心!
翻译自: https://scotch.io/tutorials/making-mean-apps-with-google-maps-part-ii
mean shift应用
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/146183.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...