סקירה כללית
במדריך הזה מוסבר איך להשתמש במקבצי סמנים כדי להציג מספר גדול של סמנים במפה. אפשר להשתמש בספרייה @googlemaps/markerclusterer בשילוב עם Maps JavaScript API כדי לשלב סמנים שקרובים זה לזה באשכולות, ולפשט את הצגת הסמנים במפה.
במפה הבאה אפשר לראות איך פועל קיבוץ הסמנים.
המספר שמופיע על אשכול מציין כמה סמנים הוא מכיל. שימו לב שככל שמתקרבים לאחד מהמיקומים של האשכול, המספר שמופיע על האשכול קטן ומתחילים לראות את הסמנים הנפרדים במפה. אם מקטינים את התצוגה של המפה, הסמנים מתאחדים שוב לקבוצות.
בדוגמה הבאה מוצג הקוד המלא שצריך כדי ליצור את המפה הזו.
TypeScript
import { MarkerClusterer } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); const mapElement = document.querySelector<google.maps.MapElement>('gmp-map')!; const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); // markers can only be keyboard focusable when they have click listeners // open info window when marker is clicked marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
JavaScript
import { MarkerClusterer } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); const mapElement = document.querySelector('gmp-map'); const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); // markers can only be keyboard focusable when they have click listeners // open info window when marker is clicked marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
CSS
html, body { height: 100%; margin: 0; padding: 0; }
HTML
<html>
<head>
<title>Marker Clustering</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<script>
// prettier-ignore
(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: "GOOGLE_MAPS_API_KEY",
});
</script>
</head>
<body>
<gmp-map
center="-28.024,140.887"
zoom="3"
map-id="DEMO_MAP_ID"></gmp-map>
</body>
</html>לשם המחשה, במדריך הזה מוסיפים קבוצה של סמנים למפה באמצעות המערך locations. אתם יכולים להשתמש במקורות אחרים כדי לקבל סמנים למפה.
מידע נוסף זמין במדריך בנושא יצירת סמנים.
הוספת מקבץ סמנים
כדי להוסיף כלי לאיגוד סמנים:
- מוסיפים את ספריית מקבצי הסמנים לדף או לאפליקציה. הספרייה זמינה ב-NPM בכתובת
@googlemaps/markerclusterer
ובמאגר ב-GitHub.
בוחרים באחת מהאפשרויות הבאות כדי להתקין את הספרייה:
NPM
מתקינים את הספרייה @googlemaps/markerclusterer באמצעות NPM.
npm install @googlemaps/markerclusterer
CDN
הסקריפט הבא טוען את הספרייה מ-unpkg.com CDN.
<script src="https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js"></script>
- מוסיפים מקבץ סמנים באפליקציה.
הקוד הבא מוסיף למפה מקבץ סמנים. בוחרים באחת מהאפשרויות הבאות:
NPM
import { MarkerClusterer } from "@googlemaps/markerclusterer"; const markerCluster = new MarkerClusterer({ markers, map });
CDN
כשניגשים לספרייה באמצעות ה-CDN, היא זמינה בקטע
markerClustererglobal.const markerCluster = new markerClusterer.MarkerClusterer({ markers, map });
בדוגמה הזו, המערך
markersמועבר אלMarkerClusterer. - (אופציונלי) מתאימים אישית את מקבץ הסמנים כדי לשנות את המראה של סמל המקבץ, את אלגוריתם הקיבוץ או את אופן הפעולה של הקליק.
התאמה אישית של מקבץ סמנים
אפשר להתאים אישית את MarkerClusterer על ידי העברת אפשרויות נוספות באובייקט MarkerClustererOptions כשיוצרים את המכונה של האשכול:
-
renderer: קובעת איך סמני האשכול מוצגים במפה. -
algorithmו-algorithmOptions: קובעים איך הסמנים מקובצים לאשכולות על סמך המרחק ורמת הזום. -
onClusterClick: מבטל את התנהגות ברירת המחדל כשמשתמש לוחץ על סמן של אשכול.
במפה הבאה מוצגים סמנים עם סמלי אשכולות בהתאמה אישית.
התאמה אישית של סמל האשכול
כברירת מחדל, MarkerClusterer משתמש ב-DefaultRenderer כדי ליצור סמלי אשכולות מסוג SVG. כדי להתאים אישית את המראה של סמני האשכולות, מעבירים אובייקט שמטמיע את הממשק Renderer לאפשרות renderer.
הממשק Renderer דורש שיטה render(cluster, stats, map) שמחזירה AdvancedMarkerElement (או google.maps.Marker מדור קודם). השיטה render מקבלת את הארגומנטים הבאים:
-
cluster(Cluster): מייצג את קבוצת הסמנים, כולל:-
count: מספר הסמנים הגלויים באשכול. -
position: המיקום המרכזי של האשכול ב-google.maps.LatLng. -
bounds: ה-google.maps.LatLngBoundsשל האשכול. -
markers: מערך הסמנים שכלולים באשכול.
-
-
stats(ClusterStats): מספק נתונים סטטיסטיים לגבי כל האשכולות במחזור העיבוד הנוכחי, כמוstats.clusters.markers.mean,min,maxו-sum. אתם יכולים להשתמש בערכים האלה כדי לעצב אשכולות באופן דינמי על סמך הגודל היחסי שלהם. -
map(google.maps.Map): מופע המפה.
בדוגמה הבאה מוגדר renderer בהתאמה אישית שיוצר תג HTML עגול לכל AdvancedMarkerElement ומשנה את צבע הרקע שלו כשמספר הסמנים באשכול חורג מהגודל הממוצע של האשכול.
TypeScript
// Customize the cluster icons using a custom Renderer const renderer = { render: ( { count, position, }: { count: number; position: google.maps.LatLng }, stats: ClusterStats ) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); }
JavaScript
// Customize the cluster icons using a custom Renderer const renderer = { render: ({ count, position }, stats) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); }
דוגמה מלאה של רכיב רינדור בהתאמה אישית
TypeScript
import { MarkerClusterer, ClusterStats } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); await customElements.whenDefined('gmp-map'); const mapElement = document.querySelector<google.maps.MapElement>('gmp-map')!; const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Customize the cluster icons using a custom Renderer const renderer = { render: ( { count, position, }: { count: number; position: google.maps.LatLng }, stats: ClusterStats ) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
JavaScript
import { MarkerClusterer } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); await customElements.whenDefined('gmp-map'); const mapElement = document.querySelector('gmp-map'); const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Customize the cluster icons using a custom Renderer const renderer = { render: ({ count, position }, stats) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
CSS
html, body, gmp-map { height: 100%; margin: 0; padding: 0; } .custom-cluster-icon { width: 45px; height: 45px; border-radius: 50%; border: 2px solid white; display: flex; justify-content: center; align-items: center; color: white; font-weight: bold; font-size: 14px; font-family: sans-serif; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); } .large-cluster { background-color: #ff0000; } .small-cluster { background-color: #0000ff; }
HTML
<html>
<head>
<title>marker-clustering-custom</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<script>
// prettier-ignore
(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: "GOOGLE_MAPS_API_KEY"
});
</script>
</head>
<body>
<gmp-map
center="-28.024, 140.887"
zoom="3"
map-id="DEMO_MAP_ID"></gmp-map>
</body>
</html>דוגמאות נוספות לשימוש ברכיב ה-renderer מופיעות בהדגמה של השימוש ברכיב ה-renderer.
שינוי אלגוריתם האשכולות
כברירת מחדל, MarkerClusterer משתמש ב-SuperClusterAlgorithm, שיוצר אשכולות של סמנים באמצעות עצי k-d. אפשר להתאים אישית את התנהגות האשכולות על ידי העברת מופע של אלגוריתם לאפשרות algorithm, או על ידי העברת אפשרויות לאלגוריתם ברירת המחדל לאפשרות algorithmOptions.
הספרייה כוללת את האלגוריתמים המובנים הבאים:
-
SuperClusterAlgorithm(ברירת מחדל): אשכולות מהירים של נקודות גיאו-מרחביות באמצעות עצים k-d. אפשרויות נפוצות:-
maxZoom: רמת הזום המקסימלית שבה נוצרים אשכולות (ברירת המחדל:16). ברמות זום שגבוהות מ-maxZoom, סמנים בודדים מוצגים בלי אשכולות. -
radius: רדיוס האשכול בפיקסלים (ברירת המחדל:60). -
minPoints: המספר המינימלי של סמנים שנדרשים כדי ליצור אשכול (ברירת המחדל:2).
-
SuperClusterViewportAlgorithm: גרסה שלSuperClusterAlgorithmעם סינון לפי אזור תצוגה, שיוצרת אשכולות של סמנים רק בתוך אזור התצוגה הנוכחי של המפה, בתוספת מרווח פנימי בפיקסלים (viewportPadding, ברירת המחדל:60).-
GridAlgorithm: אלגוריתם אשכולות מבוסס-רשת. האפשרויות הנפוצות כוללות אתmaxZoom(ברירת המחדל:16),gridSize(גודל הרשת בפיקסלים, ברירת המחדל:40) ו-maxDistance(המרחק המקסימלי במטרים בין מרכז האשכול לסמן, ברירת המחדל:40000). -
NoopAlgorithm: רינדור של כל הסמנים בלי לקבץ אותם.
בדוגמה הבאה מגדירים את SuperClusterAlgorithm עם radius ו-maxZoom בהתאמה אישית:
import {MarkerClusterer, SuperClusterAlgorithm} from '@googlemaps/markerclusterer'; const markerCluster = new MarkerClusterer({ map, markers, algorithm: new SuperClusterAlgorithm({ radius: 80, maxZoom: 15, }), });
כדי להשוות בין התנהגות האלגוריתמים השונים, אפשר לעיין בהדגמה של השוואה בין אלגוריתמים.
התאמה אישית של התנהגות הקליקים על אשכולות
כברירת מחדל, כשלוחצים על סמן של אשכול, אזור התצוגה של המפה מותאם לגבולות של האשכול שעליו לחצו
(map.fitBounds(cluster.bounds)). כדי להתאים אישית את מה שקורה כשמשתמש לוחץ על אשכול, מעבירים פונקציית קריאה חוזרת לאפשרות onClusterClick:
const markerCluster = new MarkerClusterer({ map, markers, onClusterClick: (event, cluster, map) => { // Zoom in by 2 levels centered on the cluster position map.setCenter(cluster.position); map.setZoom((map.getZoom() ?? 3) + 2); }, });
מידע נוסף
אתם יכולים לראות דוגמאות מורכבות יותר של קיבוץ סמנים במאגר ב-GitHub ולקרוא את מאמרי העזרה של הספרייה.