Ứng dụng “tìm quanh đây” với MongoDB – Geospatial Queries, Google Map API
Mongodb có hỗ trợ các phép truy vấn trên dữ liệu không gian địa lý. Chúng ta sẽ áp dụng tính năng này để xây dựng một ứng dụng lưu trữ và chia sẻ địa điểm. Ứng dụng sẽ các chức năng:
- Người dùng đánh dấu các điểm trên bản đồ.
- Hiển thị các điểm đã được dánh dấu trong phạm vi “quanh đây”
Những thứ chúng sẽ phải động tới:
- Mongodb
- Nodejs, express, moongoose
- Google Map API
- …
Demo: https://demo-mongo-geospatial.herokuapp.com
Bắt đầu
Ứng dụng:
- Thêm mới một điểm:
- Click chuột phải lên một điểm trên map, hoặc kéo thả marker có trước đó tới điểm cần đánh dấu
- Nhập thông tin mô tả về địa điểm đó
- Chọn danh mục cho điểm đó
- Nhấn
Submit
- Tìm kiếm địa điểm đã được chia sẻ “quanh đây”: Tại khung
Tìm quanh đây
- Chọn danh mục những điểm sẽ tìm, chọn
Tất cả
để tìm tất cả các danh mục - Chọn điểm làm trung tâm: Vị trí của mình, hoặc điểm được đánh dấu trên map
- Click vào marker để xem mô tả về địa điểm được chia sẻ.
- Chọn danh mục những điểm sẽ tìm, chọn
Xây dựng api cho ứng dụng
Cấu trúc thư mục và cài đặt các package có thể cần dùng
npm init -y
npm install express body-parser cors mongoose -S
npm install jquery boostrap@3 -D
Cấu trúc thư mục
Server express cơ bản
server.js
const express = require('express'); const app = express(); const bodyParser = require('body-parser'); const cors = require('cors'); const mongoose = require('mongoose'); mongoose.connect(process.env.MONGODB_URI || 'mongodb://192.168.99.100:27017/zalo', {useMongoClient: true}, (err) => { if (err) throw err; }); const PORT = process.env.PORT || 8080; const whitelist = process.env.WHITELIST ? process.env.WHITELIST.split(',') : true; const corsOptions = { origin: Array.isArray(whitelist) ? ((origin, callback) => { if (whitelist.indexOf(origin) !== -1) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }) : true }; app.use(cors(corsOptions)); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json({ type: '*/*' })); /* Routes */ app.use('/', require('./controller')); app.listen(PORT); console.log('Server is runing on port: ' + PORT); module.exports = app;
Chúng ta sẽ public ứng dụng online nên việc cài đặt witelist origin mình nghĩ là cần thiết :man_detective:
Định nghĩa Schema cho Collection
location.js
const mongoose = require('mongoose'); let LocationSchema = new Schema({ category: { type: String, index: true, unique: false, require: [true, 'Why no category? We have "Other" category.'], }, desc: { type: String, require: false, }, loc: { type: [Number], // [<longitude>, <latitude>] index: '2d' // create the geospatial index } }, { timestamps: true }); // register the mongoose model module.exports = mongoose.model('Location', LocationSchema);
Thông tin vị trí sẽ được lưu trữ trong thuộc tính loc
, dữ liệu của trường này sẽ là tọa độ của điểm địa lý [ lon, lat]
, thuộc tính này được đánh index là 2d
chi tiết bạn có thể tìm hiểu thêm ở đây
Định nghĩa các route và logic cho ứng dụng
controller.js
const router = require('express').Router(); const path = require('path'); const Location = require('./location'); router.route('/') .get((req, res) => { res.sendFile(path.join(__dirname, 'asset', 'index.html')); }); router.route('/api/locations') // Get location(s) near by me .get((req, res) => { let limit = req.query.limit || 50; // get the max distance or set it to 5 kilometers let maxDistance = req.query.distance || 5; // we need to convert the distance to radians // the raduis of Earth is approximately 6371 kilometers // Just to be more exact since I just researched for this and this can clearly help future visitors, the actual formula is : 1° latitude = 69.047 miles = 111.12 kilometers // https://stackoverflow.com/questions/5319988/how-is-maxdistance-measured-in-mongodb maxDistance /= 111.12; // get coordinates [ <longitude> , <latitude> ] let coords = []; coords[0] = req.query.lon || 0; coords[1] = req.query.lat || 0; let query = { loc: { $near: coords, $maxDistance: maxDistance } }; if (req.query.category) { query.category = req.query.category; } // find location(s) Location .find(query) .limit(limit) .exec((err, locations) => { if (err) { return res.status(400).json({message: err.message}); } res.json(200, {locations}); }); }) // Create a location .post((req, res) => { let localtion = new Location({ category: req.body.category || 'other', desc: req.body.desc, loc: [req.body.lng, req.body.lat] }); localtion.save((err) => { if (err) { res.status(400).json({message: err.message}); return; } res.json({message: 'Location created!'}) }) }); module.exports = router;
Lưu ý thuộc tính loc là một number array có 2 phần tử, phần tử index 0 là chỉ số longitude, index 1 là latitude Như vậy chúng ta đã hoàn thành api cho ứng dụng, tiếp đến chúng ta sẽ xây dựng phần giao diện.
Giao diện người dùng
Google map api
Chúng ta cần có một google app có bật google map api
để sử dụng được map api. Bạn cần chú ý thiết lập những domain được sử API Key này (Accept requests from these HTTP referrers).
Giao diện người dùng
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Tìm quanh đây</title> <link rel="stylesheet" href="css/boostrap/css/bootstrap.min.css"> <link rel="stylesheet" href="css/style.css"> <script src="js/jquery.min.js"></script> <script src="js/script.js"></script> <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"> </script> </head> <body class="container"> <div class="panel panel-success"> <div class="panel-heading"> Thêm địa điểm </div> <div class="panel-body"> <form onsubmit="return false"> <div class="form-group"> <label for="desc">Mô tả</label> <textarea class="form-control" id="desc" rows="3"></textarea> </div> <div class="form-group"> <label for="category">Danh mục</label> <select class="form-control" id="category"> <option value="famous_area">Địa điểm nổi tiếng</option> <option value="food_drink">Ăn uống</option> <option value="park">Vui chơi</option> <option value="other">Khác</option> </select> </div> <div class="form-group"> <label for="category_find">Location</label> <input class="form-control" type="text" placeholder="Location" id="location" readonly> </div> <button type="submit" id="btn_submit" class="btn btn-primary">Submit</button> </form> </div> </div> <div class="panel-success"> <div class="panel-heading"> Tìm quanh đây </div> <div class="panel-body"> <form onsubmit="return false"> <div class="form-group"> <label for="category_find">Danh mục</label> <select class="form-control" id="category_find"> <option selected>Tất cả</option> <option value="famous_area">Địa điểm nổi tiếng</option> <option value="food_drink">Ăn uống</option> <option value="park">Vui chơi</option> <option value="other">Khác</option> </select> </div> <div class="form-group"> <label for="center_point">Tìm theo</label> <select class="form-control" id="center_point"> <option value="current">Vị trí của tôi</option> <option value="marker">Điểm dánh dấu</option> </select> </div> <button type="submit" id="btn_find" class="btn btn-primary">Tìm</button> </form> </div> </div> <div class="panel-success"> <div class="panel-heading"> Bản đồ </div> <div class="panel-body" id="map"> </div> </div> </body> </html>
Xử lý sự kiện người dùng
Chúng ta có một file script.js
xử lý các hành vi người dùng, nội dung mình sẽ chú thích trong code script.js
let map, infoWindow, marker; // Hàm này được thự hiện khi trình duyệt đã load thành công google map sdk function initMap() { map = new google.maps.Map(document.getElementById('map'), { center: {lat: -34.397, lng: 150.644}, zoom: 12 }); infoWindow = new google.maps.InfoWindow; // Lấy vị trí hiện tại, người dùng phải cho phép trình duyệt truy cập thông tin vị trí // Try HTML5 geolocation. if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(function (position) { let pos = { lat: position.coords.latitude, lng: position.coords.longitude }; infoWindow.setPosition(pos); infoWindow.setContent('Bạn đang ở đây!'); infoWindow.open(map); map.setCenter(pos); //Add listener, Sự kiện click chuột phải lên map, tạo ra một marker google.maps.event.addListener(map, "rightclick", mapOnRightClick); }, function () { handleLocationError(true, infoWindow, map.getCenter()); }); } else { // Browser doesn't support Geolocation handleLocationError(false, infoWindow, map.getCenter()); } } function handleLocationError(browserHasGeolocation, infoWindow, pos) { infoWindow.setPosition(pos); infoWindow.setContent(browserHasGeolocation ? 'Error: The Geolocation service failed.' : 'Error: Your browser doesn\'t support geolocation.'); infoWindow.open(map); } function mapOnRightClick(event) { let lat = event.latLng.lat(); let lng = event.latLng.lng(); addUserMarker(lat, lng); } // Tạo ra một marker khi người dùng click chuột phải lên map tại vị trí click function addUserMarker(lat, lng) { let latlng = new google.maps.LatLng(lat, lng); if (marker) { marker.setPosition(latlng) } else { let location = {lat, lng}; marker = new google.maps.Marker({ position: location, label: 'A', draggable: true, map: map }); // xử lý dự kiện khi kéo thả marker marker.addListener('dragend', function (event) { $('#location').val(`[${event.latLng.lat()}, ${event.latLng.lng()}]`); }); } $('#location').val(`[${lat}, ${lng}]`); } // app let markers = [], circle; // Xóa danh sách các marker đang có trên map function clearMarkers() { if (circle) { circle.setMap(null); } markers.forEach(function (m) { m.setMap(null); }); } let icons = { 'other': '/img/ic_other.png', 'food_drink': '/img/ic_food_drink.png', 'famous_area': '/img/ic_famous_area.png', 'park': '/img/ic_park.png', }; // Adds a marker to the map. function addMarker(lat, lng, content, cate) { let position = {lat, lng}; let marker = new google.maps.Marker({ position, icon: icons[cate] || icons['other'], map: map }); let infowindow = new google.maps.InfoWindow({ content: content || "Không có mô tả." }); marker.addListener('click', function() { infowindow.open(map, marker); }); return marker; } $(document).ready(function () { // Khi click nút thêm mới một điểm $('#btn_submit').on('click', function (event) { event.preventDefault(); if (!marker) { return; } let category = $('#category').val(); let desc = $('#desc').val(); let location = JSON.parse($('#location').val()); $.ajax({ type: "POST", url: '/api/locations', data: { category, desc, lat: location[0], lng: location[1] }, success: function(data) { console.log('onSuccess', data); } }); }); // Khi click nút tìm kiếm $('#btn_find').on('click', function (event) { event.preventDefault(); (new Promise(function (res, rej) { if ($('#center_point').val() === 'marker') { let location = JSON.parse($('#location').val()); res({lat: location[0], lng: location[1]}); return; } if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(function (position) { let pos = { lat: position.coords.latitude, lng: position.coords.longitude }; map.setCenter(pos); res(pos) }, function () { rej('Fail to get current position'); }); } else { rej('Browser doesn\'t support Geolocation') } })) .then(function (pos) { $.ajax({ type: "GET", url: '/api/locations', data: { lat: pos.lat, lng: pos.lng, }, success: function(data) { clearMarkers(); marker && marker.setPosition(pos); circle = new google.maps.Circle({ strokeColor: '#ff0014', strokeOpacity: 0.8, strokeWeight: 1.3, fillColor: '#1fff00', fillOpacity: 0.2, map: map, center: pos, radius: 5000 }); data.locations.forEach(function (loc) { markers.push(addMarker(loc.loc[1], loc.loc[0], loc.desc, loc.category)); }); } }); }) .catch(function (err) { console.log(err); }) }); });
Cuối cùng là file style style.css
#map { height: 800px; width: 100%; }
Kết thúc
Hy vọng bài viết sẽ giúp mọi người có thêm các từ khóa và các ý tưởng để xây dựng các ý tưởng cho mình.
Sourcecode: Github