概要
現代のオンライン食事注文システムにおいて、ユーザーが自身の現在地から近隣店舗を検索する機能は不可欠です。ここでは、地図 API(本稿では百度地図 API)を活用して、店舗の位置登録、配達エリアの定義、および条件に基づく検索ロジックを実装する方法について解説します。
店舗位置のマーキング機能
店舗管理画面では、管理者が地図上で正確な営業場所を指定できる必要があります。これは、画面上にマーカーを表示し、ドラッグ操作によって座標を更新する仕組みで実現されます。
以下のコードは、初期化された地図インスタンス上にマーカーを作成し、クリックおよびドラッグイベントにより座標データを取得して保存する処理を示しています。
<script type="text/javascript">
// 定数定義
const INITIAL_ZOOM_LEVEL = 15;
let currentCenterPoint = null;
let activeMarker = null;
// 地図インスタンスの生成
const mapInstance = new BMap.Map("map_canvas");
mapInstance.enableScrollWheelZoom();
// 初期座標取得
const initialLat = parseFloat($("#hidLat").val());
const initialLng = parseFloat($("#hidLng").val());
currentCenterPoint = new BMap.Point(initialLng, initialLat);
// カスタムアイコン設定
const customIcon = new BMap.Icon(
"/images/custom_marker.png",
new BMap.Size(20, 34),
{ anchor: new BMap.Size(10, 0) }
);
// マーカークラスの初期化
function initStoreMarker(point) {
if (activeMarker) {
mapInstance.removeOverlay(activeMarker);
}
activeMarker = new BMap.Marker(point, { icon: customIcon });
mapInstance.addOverlay(activeMarker);
activeMarker.enableDragging();
activeMarker.setTitle("位置を調整してください");
setupMarkerEvents(activeMarker);
}
// イベントリスナーの設定
function setupMarkerEvents(marker) {
marker.addEventListener("dragend", function(e) {
const newPos = e.point;
saveCoordinates(newPos);
openInfoWindow(newPos);
});
marker.addEventListener("dragstart", function() {
mapInstance.closeInfoWindow();
});
}
// 地図全体をクリックした時のハンドラ
mapInstance.addEventListener("click", function(e) {
const clickPoint = e.point;
updateCenterAndMarker(clickPoint);
});
function updateCenterAndMarker(point) {
mapInstance.clearOverlays();
currentCenterPoint = point;
initStoreMarker(point);
}
// 座標を隠しフィールドへ反映
function saveCoordinates(point) {
document.getElementById("hidLat").value = point.lat;
document.getElementById("hidLng").value = point.lng;
}
// 初期表示用の情報ウィンドウ
const infoWindowOptions = { width: 250, height: 50 };
const windowContent = "<div><p>位置確認後、「保存」ボタンを<br><input type='button' value='確定' onclick='saveAndClose()' /></p></div>";
const infoWindow = new BMap.InfoWindow(windowContent, infoWindowOptions);
function openInfoWindow(point) {
activeMarker.openInfoWindow(infoWindow);
}
function saveAndClose() {
mapInstance.closeInfoWindow();
// 保存処理へ遷移
}
// 初期マーカー生成
initStoreMarker(currentCenterPoint);
</script>
配送エリアのポリゴン描画
店舗ごとに特定の区域(例:政令指定都市内など)を設定する場合、地図上多边形状(ポリゴン)を描画する機能が必要になります。複数の点を配置し、それらを結合して領域を定義します。
<script type="text/javascript">
const DELIVERY_MAP_ID = "delivery_map";
let polygonPoints = [];
let boundaryMarkers = [];
let mapObj = new BMap.Map(DELIVERY_MAP_ID);
mapObj.enableScrollWheelZoom();
const zoneIcon = new BMap.Icon("/images/red_pin.png", new BMap.Size(14, 22));
// ポリゴンの再描画関数
function renderPolygon() {
if (polygonPoly) mapObj.removeOverlay(polygonPoly);
polygonPoints = boundaryMarkers.map(m => m.getPosition());
if (polygonPoints.length > 2) {
polygonPoly = new BMap.Polygon(polygonPoints, {
strokeColor: "#ff0000",
strokeWeight: 2,
strokeOpacity: 0.5,
fillColor: "#ffcccc"
});
mapObj.addOverlay(polygonPoly);
}
}
// 地図クリックイベント処理
mapObj.addEventListener("click", function(evt) {
const newPt = evt.point;
const marker = new BMap.Marker(newPt, { icon: zoneIcon });
marker.enableDragging();
mapObj.addOverlay(marker);
boundaryMarkers.push(marker);
// ドラッグイベント設定
["dragstart", "dragend"].forEach(ev => {
marker.addEventListener(ev, renderPolygon);
});
// クリックで削除可能にする
marker.addEventListener("click", function() {
const index = boundaryMarkers.indexOf(marker);
if (index > -1) {
mapObj.removeOverlay(marker);
boundaryMarkers.splice(index, 1);
renderPolygon();
}
});
renderPolygon();
});
// 既存データの復元
function restoreExistingArea(coordsString) {
if (!coordsString) return;
const points = coordsString.split('|').map(str => str.split(','));
points.forEach((coord, idx) => {
const pt = new BMap.Point(parseFloat(coord[1]), parseFloat(coord[0]));
const mk = new BMap.Marker(pt, { icon: zoneIcon });
mk.enableDragging();
mapObj.addOverlay(mk);
["dragstart", "dragend"].forEach(ev => {
mk.addEventListener(ev, renderPolygon);
});
mk.addEventListener("click", () => {
const i = boundaryMarkers.indexOf(mk);
if(i>-1){boundaryMarkers.splice(i,1); renderPolygon();}
});
boundaryMarkers.push(mk);
});
renderPolygon();
}
// 最終データ取得
function getFormattedPolygonData() {
if (boundaryMarkers.length === 0) return "";
return boundaryMarkers.map(m => {
const pos = m.getPosition();
return `${pos.lat},${pos.lng}`;
}).join("|");
}
</script>
検索アルゴリズムの採用
ユーザー側の検索機能には主に 2 つのアプローチがあります。一つは中心地点からの距離による検索、もう一つは前述のポリゴン区域内にあるか否かの判定です。
マップラボックス検索
ユーザーが地図上で矩形範囲を選択した場合、その中心点と半径を計算して円形検索に変換する処理を行います。
// 選択された矩形の端点
const nePoint = northEastCorner; // 北東
const swPoint = southWestCorner; // 南西
// 境界値の最小・最大値を取得
const minLng = Math.min(nePoint.lng, swPoint.lng);
const maxLng = Math.max(nePoint.lng, swPoint.lng);
const minLat = Math.min(nePoint.lat, swPoint.lat);
const maxLat = Math.max(nePoint.lat, swPoint.lat);
// 中心点計算
const centerX = (minLng + maxLng) / 2;
const centerY = (minLat + maxLat) / 2;
// 半径計算(概算:1 度=約 111km)
const latDiff = (maxLat - minLat) / 2;
const lngDiff = (maxLng - minLng) / 2;
const approxRadiusKm = Math.max(latDiff, lngDiff) * 111;
// データ取得関数呼び出し
fetchNearbyShops(centerX, centerY, approxRadiusKm);
配送エリア内判定(ライ法)
ユーザーの位置が、店舗が定義した不規則な配送エリア(ポリゴン)内に含まれるかを判定するために、レイキャスティング法(Ray Casting Algorithm)を用います。このロジックはサーバーサイド(C#)で処理するのが一般的です。
using System.Collections.Generic;
using System.Linq;
namespace DeliverySystem.Geometry
{
public static class PolygonChecker
{
private const double EPSILON = 1e-5;
/// <summary>
/// ポリゴン内部判定処理
/// リターン値: 0=内部, 1=線上, 2=外部
/// </summary>
public static int CheckPointInPolygon(List<Point> vertices, Point target)
{
if (vertices.Count < 3) return 2;
int intersections = 0;
for (int i = 0, j = vertices.Count - 1; i < vertices.Count; j = i++)
{
var p1 = vertices[i];
var p2 = vertices[j];
if (IsOnSegment(p1, p2, target)) return 1;
// X 軸と線分が交わるかどうかを判定
bool cond1 = ((p1.Y > target.Y) != (p2.Y > target.Y));
// X 座標の比較
double xIntersect = (target.Y - p1.Y) * (p2.X - p1.X) / (double)(p2.Y - p1.Y) + p1.X;
if (target.X < xIntersect)
{
intersections++;
}
}
return (intersections % 2 == 1) ? 0 : 2;
}
private static bool IsOnSegment(Point a, Point b, Point c)
{
double crossProduct = (c.X - a.X) * (b.Y - a.Y) - (c.Y - a.Y) * (b.X - a.X);
if (Math.Abs(crossProduct) > EPSILON) return false;
double dotProduct = (c.X - a.X) * (b.X - a.X) + (c.Y - a.Y) * (b.Y - a.Y);
double squaredLength = (b.X - a.X) * (b.X - a.X) + (b.Y - a.Y) * (b.Y - a.Y);
return dotProduct >= 0 && dotProduct <= squaredLength;
}
// 座標データ構造
public struct Point
{
public double X;
public double Y;
public Point(double x, double y) { X = x; Y = y; }
}
}
}