Ứ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ẻ.

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

Từ khóa: , ,