diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index 8bf3d432..f6f90837 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -396,7 +396,7 @@ def create_editor_form(editor_model): 'color_background', 'color_wall_fill', 'color_wall_border', 'color_door_fill', 'color_ground_fill', 'color_obstacles_default_fill', 'color_obstacles_default_border', 'stroke_color', 'stroke_width', 'stroke_opacity', 'fill_color', 'fill_opacity', 'interactive', 'point_icon', 'extra_data', 'show_label', - 'show_geometry', 'show_label', 'show_geometry', 'default_geomtype', + 'show_geometry', 'show_label', 'show_geometry', 'default_geomtype', 'cluster_points', "load_group_display", "load_group_contribute", "altitude_quest", ] diff --git a/src/c3nav/mapdata/migrations/0129_dataoverlay_cluster_points.py b/src/c3nav/mapdata/migrations/0129_dataoverlay_cluster_points.py new file mode 100644 index 00000000..28f5c31d --- /dev/null +++ b/src/c3nav/mapdata/migrations/0129_dataoverlay_cluster_points.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2024-12-26 02:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0128_space_identifyable'), + ] + + operations = [ + migrations.AddField( + model_name='dataoverlay', + name='cluster_points', + field=models.BooleanField(default=False, verbose_name='cluster points together when zoomed out'), + ), + ] diff --git a/src/c3nav/mapdata/models/overlay.py b/src/c3nav/mapdata/models/overlay.py index bdb93c89..e6b47c53 100644 --- a/src/c3nav/mapdata/models/overlay.py +++ b/src/c3nav/mapdata/models/overlay.py @@ -26,6 +26,8 @@ class DataOverlay(TitledMixin, AccessRestrictionMixin, models.Model): fill_color = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('default fill color')) fill_opacity = models.FloatField(blank=True, null=True, verbose_name=_('fill opacity')) + cluster_points = models.BooleanField(default=False, verbose_name=_('cluster points together when zoomed out')) + default_geomtype = models.CharField(max_length=255, blank=True, null=True, choices=GeometryType, verbose_name=_('default geometry type')) pull_url = models.URLField(blank=True, null=True, verbose_name=_('pull URL')) diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index 00567377..f829f41e 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -375,6 +375,7 @@ class DataOverlaySchema(TitledSchema, DjangoModelSchema): stroke_opacity: Optional[float] fill_color: Optional[str] fill_opacity: Optional[float] + cluster_points: bool diff --git a/src/c3nav/site/static/site/css/c3nav.scss b/src/c3nav/site/static/site/css/c3nav.scss index 85931f3b..970d4817 100644 --- a/src/c3nav/site/static/site/css/c3nav.scss +++ b/src/c3nav/site/static/site/css/c3nav.scss @@ -1880,18 +1880,6 @@ blink { } } -.overlay-point-icon { - > span { - display: inline-block; - width: 24px; - height: 24px; - line-height: 24px; - text-align: center; - font-size: 24px; - font-family: 'Material Symbols Outlined'; - } -} - .data-overlay-popup { .leaflet-popup-content { margin: 0; @@ -1944,7 +1932,8 @@ blink { } } -.quest-icon { +.symbol-icon { + --icon-color: var(--color-primary); > span { display: inline-block; width: 30px; @@ -1954,15 +1943,33 @@ blink { font-size: 22px; font-family: 'Material Symbols Outlined'; background-color: white; - color: var(--color-primary); + color: var(--icon-color); border-radius: 100%; - box-shadow: 0 0 0 5px color-mix(in srgb, transparent, var(--color-primary) 60%); + box-shadow: 0 0 0 5px color-mix(in srgb, transparent, var(--icon-color) 60%); transition: color, background-color 150ms ease-in-out; - &:hover { - background-color: var(--color-primary); - color: white; + cursor: default; + } + + + &.symbol-icon-interactive { + > span { + cursor: pointer; + + &:hover { + background-color: var(--icon-color); + color: white; + } + } + } + + &.symbol-icon-empty { + > span { + width: 14px; + height: 14px; + line-height: 14px; + font-size: 10px; } } } diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 260c50cc..6324762c 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -2775,7 +2775,7 @@ QuestsControl = ExpandingControl.extend({ pointToLayer: (geom, latlng) => { return L.marker(latlng, { icon: L.divIcon({ - className: 'quest-icon', + className: 'symbol-icon symbol-icon-interactive', html: `${quest_icon}`, iconSize: [24, 24], iconAnchor: [12, 12], @@ -3130,6 +3130,7 @@ class DataOverlay { this.id = options.id; this.title = options.title; this.group = options.group; + this.cluster_points = options.cluster_points; this.default_stroke_color = options.stroke_color; this.default_stroke_width = options.stroke_width; this.default_stroke_opacity = options.stroke_opacity; @@ -3144,7 +3145,11 @@ class DataOverlay { for (const feature of features) { const level_id = feature.level_id; if (!(level_id in levels)) { - levels[level_id] = L.layerGroup([]); + if (this.cluster_points) { + levels[level_id] = L.markerClusterGroup(); + } else { + levels[level_id] = L.layerGroup(); + } } const style = { 'color': feature.stroke_color ?? this.default_stroke_color ?? 'var(--color-map-overlay)', @@ -3157,12 +3162,21 @@ class DataOverlay { style, interactive: feature.interactive, pointToLayer: (geom, latlng) => { + return L.marker(latlng, { + title: feature.title, + icon: L.divIcon({ + className: 'symbol-icon ' + (feature.point_icon ? '' : 'symbol-icon-empty ') + (feature.interactive ? 'symbol-icon-interactive' : ''), + html: `${feature.point_icon ?? ''}`, + iconSize: [24, 24], + iconAnchor: [12, 12], + }) + }); if (feature.point_icon !== null) { return L.marker(latlng, { title: feature.title, icon: L.divIcon({ - className: 'overlay-point-icon', - html: `${feature.point_icon}`, + className: 'symbol-icon ' + (feature.interactive ? 'symbol-icon-interactive' : ''), + html: `${feature.point_icon}`, iconSize: [24, 24], iconAnchor: [12, 12], }) @@ -3173,27 +3187,30 @@ class DataOverlay { ...style }); } + }, + onEachFeature: (f, layer) => { + if (feature.interactive) { + layer.bindPopup(() => { + let html = `

${feature.title}

`; + if (feature.external_url != null) { + html += `open external link`; + } + if (feature.extra_data != null) { + html += ''; + for (const key in feature.extra_data) { + html += ``; + } + + html += '
${key}${feature.extra_data[key]}
'; + } + return html; + }, { + className: 'data-overlay-popup' + }); + } } }); - if (feature.interactive) { - layer.bindPopup(() => { - let html = `

${feature.title}

`; - if (feature.external_url != null) { - html += `open external link`; - } - if (feature.extra_data != null) { - html += ''; - for (const key in feature.extra_data) { - html += ``; - } - html += '
${key}${feature.extra_data[key]}
'; - } - return html; - }, { - className: 'data-overlay-popup' - }); - } levels[level_id].addLayer(layer); }