Map: add cluster feature

This commit is contained in:
Jaeeun.Cho 2025-11-17 14:54:34 -05:00
parent 6c0df1994b
commit ed07e95c9e
1 changed files with 216 additions and 11 deletions

View File

@ -98,7 +98,7 @@
<script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))}) <script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
({key: "AIzaSyDg9u03mGrBhyOisp7VGc27CTPI9QXp8sY", v: "weekly"});</script> ({key: "AIzaSyDg9u03mGrBhyOisp7VGc27CTPI9QXp8sY", v: "weekly"});</script>
<script src="https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js"></script>
<!-- New --> <!-- New -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
@ -1248,13 +1248,14 @@ function popup(){
} }
var rstSaveInput = function(json) { var rstSaveInput = function(json) {
$('#map-modal-input .close').trigger('click'); $('#map-modal-input').modal('hide'); // Bootstrap 정상 닫기 시도
jQuery(".modal").removeClass("show");; $('#map-modal-input').removeClass('show in').css('display', 'none').attr('aria-hidden', 'true'); // 남아 있을 수 있는 클래스 제거
jQuery(".modal-backdrop").remove(); $('.modal-backdrop').remove(); // backdrop 제거
showPopupMessage(json.msg); showPopupMessage(json.msg);
//drawPoint(); //drawPoint();
updatePickupQty(); updatePickupQty();
} }
function replaceAll(str, searchStr, replaceStr) { function replaceAll(str, searchStr, replaceStr) {
@ -1593,7 +1594,17 @@ function popup(){
var marker = []; var marker = [];
var markerwindow = []; var markerwindow = [];
var overlay = []; var overlay = [];
var clusterer = null;
var clusterInfoGlobal = null;
var typeOrder = { "#FF0000": 1, "#800080": 2, "#FF80FF": 3, "#7B7A7A": 4 }; // Request > Scheduled > Normal > Finished
var rstInqPoint = function(json) { var rstInqPoint = function(json) {
if (clusterInfoGlobal) {
clusterInfoGlobal.close();
clusterInfoGlobal = null;
}
for(let j=0; j < marker.length; j++) { for(let j=0; j < marker.length; j++) {
marker[j].setMap(null); marker[j].setMap(null);
marker[j] = null; marker[j] = null;
@ -1602,6 +1613,12 @@ function popup(){
overlay[j] = null; overlay[j] = null;
} }
} }
if (clusterer) {
clusterer.setMap(null);
clusterer = null;
}
marker = []; marker = [];
overlay = []; overlay = [];
let pointGeocoder = new google.maps.Geocoder(); let pointGeocoder = new google.maps.Geocoder();
@ -1678,10 +1695,11 @@ function popup(){
position: location, position: location,
map: map, map: map,
icon: icon, icon: icon,
animation: (property.rnote && /\S/.test(property.rnote))? google.maps.Animation.BOUNCE : null // google.maps.Animation.DROP animation: (property.rnote && /\S/.test(property.rnote) && property.color !== "#7B7A7A") ? google.maps.Animation.BOUNCE : null
}); });
marker[i].customType = property.type; //'flag' 또는 'map-marker' marker[i].customType = property.type; //'flag' 또는 'map-marker'
marker[i].property = property;
// Add an info window with company information // Add an info window with company information
markerwindow[i] = new google.maps.InfoWindow({ markerwindow[i] = new google.maps.InfoWindow({
@ -1693,7 +1711,10 @@ function popup(){
}); });
google.maps.event.addListener(markerwindow[i], 'domready', function() { google.maps.event.addListener(markerwindow[i], 'domready', function() {
jQuery(".gm-ui-hover-effect").css("display","none"); // InfoWindow의 닫기 버튼만 숨기기
const iwOuter = jQuery(".gm-style-iw").last();
const closeBtn = iwOuter.parent().find(".gm-ui-hover-effect");
closeBtn.css("display","none");
}); });
marker[i].addListener('mouseout', () => { marker[i].addListener('mouseout', () => {
@ -1712,6 +1733,190 @@ function popup(){
} }
// 클러스터 생성
if (marker.length > 0) {
// flag 타입만 필터링
const flagMarkers = marker.filter(m => m.customType === 'flag');
if (flagMarkers.length > 0) {
// 사이즈 줄이기
const algorithm = new markerClusterer.GridAlgorithm({
gridSize: 5,
maxDistance: 50,
});
clusterer = new markerClusterer.MarkerClusterer({
map,
markers: flagMarkers,
algorithm,
renderer: {
render: ({ count, position, markers }) => {
// 클러스터 안에 rnote 있는 flag 마커가 한 개라도 포함됐는지 확인
const hasRnoteMarker = markers.some(m =>
m.property && m.property.rnote && /\S/.test(m.property.rnote)
);
markers.sort((a, b) => {
const t1 = a.property.color || "#FF80FF";
const t2 = b.property.color || "#FF80FF";
return typeOrder[t1] - typeOrder[t2];
});
const topColor = markers[0]?.property?.color || "#4285f4";
const size = Math.min(50, 30 + Math.log(count) * 10);
return new google.maps.Marker({
position,
icon: {
url:
"data:image/svg+xml;charset=UTF-8," +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40">
<circle cx="20" cy="20" r="18"
fill="${topColor}" />
<text x="20" y="25" text-anchor="middle"
font-size="14" fill="#fff">${count}</text>
</svg>
`),
scaledSize: new google.maps.Size(40, 40),
anchor: new google.maps.Point(10, 10),
},
zIndex: Number(google.maps.Marker.MAX_ZINDEX) + count,
animation: (
// 클러스터 안에 rnote가 있으면서 finished가 아닌 마커가 1개라도 있는 경우만 Bounce
markers.some(m =>
m.property &&
m.property.rnote && /\S/.test(m.property.rnote) &&
m.property.color !== "#7B7A7A"
)
) ? google.maps.Animation.BOUNCE : null,
});
},
},
onClusterClick: (...args) => {
let clusterObj = null;
if (args.length === 1 && args[0]) {
const a = args[0];
clusterObj = a.cluster || a;
} else if (args.length >= 2) {
clusterObj = args[1];
}
if (!clusterObj) return false;
const markersInCluster =
clusterObj.markers ||
(typeof clusterObj.getMarkers === "function"
? clusterObj.getMarkers()
: []) ||
[];
const clusterCenter =
clusterObj.position ||
(typeof clusterObj.getCenter === "function"
? clusterObj.getCenter()
: null);
if (!markersInCluster.length || !clusterCenter) return false;
if (clusterInfoGlobal) {
clusterInfoGlobal.close();
}
markersInCluster.sort((a, b) => {
const t1 = a.property.color || "#FF80FF";
const t2 = b.property.color || "#FF80FF";
return typeOrder[t1] - typeOrder[t2];
});
let html =
"<div style='min-width:220px;max-height:240px;overflow:auto'>";
markersInCluster.forEach((m) => {
const p = m.property || {};
const hasRnote = p.rnote && /\S/.test(p.rnote);
html += `
<div class="cluster-item" data-marker-index="${p.index}" style="display:flex; align-items:center; margin-bottom:4px; cursor:pointer;">
<span style="width:15px; height:15px; margin-right:6px; background-color:${p.color || "#ccc"}; display:inline-block; border:1px solid #999;"></span>
<strong>${p.store ?? ""}</strong>
${
hasRnote
? `<span style="margin-left:6px; padding:2px 4px; background:#FF9800; color:white; border-radius:3px; font-size:10px;">
notice
</span>`
: ""
}
</div>
<div style="margin-left:22px; font-size:12px; color:#333;">
Est. Quantity : ${p.estqty ?? "-"}
</div>
<hr style="margin:7px 0;">
`;
});
html += "</div>";
const clusterInfo = new google.maps.InfoWindow({
content: html,
position: clusterCenter,
pixelOffset: new google.maps.Size(0, -25),
});
clusterInfoGlobal = clusterInfo;
clusterInfo.open(map);
google.maps.event.addListenerOnce(
clusterInfo,
"domready",
function () {
document.querySelectorAll(".cluster-item").forEach((el) => {
el.addEventListener("click", () => {
const idx = el.getAttribute("data-marker-index");
if (idx !== null && marker[idx]) {
google.maps.event.trigger(marker[idx], "click");
}
clusterInfo.close();
});
});
}
);
return false;
},
});
}
// 클러스터링이 끝났을 때 실행되는 이벤트
clusterer.addListener('clusteringend', () => {
marker.forEach(m => {
if (!m.getMap()) return;
if (m.property?.rnote && /\S/.test(m.property.rnote) && m.property.color !== "#7B7A7A") {
m.setAnimation(google.maps.Animation.BOUNCE);
} else {
m.setAnimation(null);
}
});
});
}
}
function showInfoWindow(position, title, quantity, infoType) {
const showClose = infoType === 'cluster'; // 클러스터일 때만 X 표시
const infoContent = `
<div class="info-window">
${showClose ? '<button class="close" onclick="closeInfoWindow()">×</button>' : ''}
<div class="info-body">
<strong>${title}</strong><br>
Est. Quantity: ${quantity}
</div>
</div>
`;
infoWindow.setContent(infoContent);
infoWindow.setPosition(position);
infoWindow.open(map);
} }