Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions map-view/src/api/__mocks__/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ export const mockMapConfig: MapConfig = {
},
traffic_sign_icons_url: "http://127.0.0.1:8000/static/traffic_control/svg/traffic_sign_icons/",
featureTypeEditNameMapping: {},
icon_scale: 0.1,
icon_type: "svg",
};
15 changes: 11 additions & 4 deletions map-view/src/common/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ class Map {
}

private createNonClusteredOverlayLayerGroup(mapConfig: MapConfig) {
const { overlayConfig, traffic_sign_icons_url } = mapConfig;
const { overlayConfig, traffic_sign_icons_url, icon_scale, icon_type } = mapConfig;
const { layers, sourceUrl } = overlayConfig;
const overlayLayers = layers
.filter(({ clustered }) => !clustered)
Expand All @@ -443,7 +443,8 @@ class Map {

const vectorLayer = new VectorLayer({
source: vectorSource,
style: (feature: FeatureLike) => getSinglePointStyle(feature, use_traffic_sign_icons, traffic_sign_icons_url),
style: (feature: FeatureLike) =>
getSinglePointStyle(feature, use_traffic_sign_icons, traffic_sign_icons_url, icon_scale, icon_type),
visible: false,
opacity: identifier.includes("plan") ? 0.5 : 1, // 100% opacity for reals, 50% opacity for plans
});
Expand All @@ -458,7 +459,7 @@ class Map {
}

private createClusteredOverlayLayerGroup(mapConfig: MapConfig) {
const { overlayConfig, traffic_sign_icons_url } = mapConfig;
const { overlayConfig, traffic_sign_icons_url, icon_scale, icon_type } = mapConfig;
const { layers, sourceUrl } = overlayConfig;
// Fetch device layers
const overlayLayers = layers
Expand Down Expand Up @@ -504,7 +505,13 @@ class Map {
styleCache[size] = style;
} else {
const feature = clusterFeature.get("features")[0];
style = getSinglePointStyle(feature, use_traffic_sign_icons, traffic_sign_icons_url);
style = getSinglePointStyle(
feature,
use_traffic_sign_icons,
traffic_sign_icons_url,
icon_scale,
icon_type,
);
styleCache[feature.get("device_type_code")] = style;
}
}
Expand Down
15 changes: 13 additions & 2 deletions map-view/src/common/MapUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ export function getSinglePointStyle(
feature: FeatureLike,
use_traffic_sign_icons: boolean,
traffic_sign_icons_url: string,
icon_scale: number,
icon_type: string,
) {
if (use_traffic_sign_icons && feature.get("device_type_code") !== null) {
// Traffic sign style
return new Style({
image: new Icon({
src: `${traffic_sign_icons_url}${feature.get("device_type_code")}.svg`,
scale: 0.075,
src: getIconSrc(traffic_sign_icons_url, icon_type, feature.get("device_type_code")),
scale: icon_scale,
}),
});
}
Expand All @@ -89,6 +91,15 @@ export function getSinglePointStyle(
return getStylesForGeometry(geometry);
}

function getIconSrc(traffic_sign_icons_url: string, icon_type: string, device_type_code: string) {
const base_src = `${traffic_sign_icons_url}${device_type_code}.svg`;
if (icon_type === "png") {
return base_src.concat(".png");
} else {
return base_src;
}
}

export function getStylesForGeometry(geometry: Geometry | RenderFeature | undefined) {
if (geometry === undefined) {
return geometryStyles["unknown"];
Expand Down
2 changes: 2 additions & 0 deletions map-view/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface MapConfig {
overviewConfig: OverviewConfig;
traffic_sign_icons_url: string;
featureTypeEditNameMapping: Record<string, string>;
icon_scale: number;
icon_type: string;
}

export interface FeatureProperties {
Expand Down
7 changes: 6 additions & 1 deletion map/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin

from map.models import FeatureTypeEditMapping, Layer
from map.models import FeatureTypeEditMapping, IconDrawingConfig, Layer


@admin.register(Layer)
Expand All @@ -22,3 +22,8 @@ class LayerAdmin(admin.ModelAdmin):
@admin.register(FeatureTypeEditMapping)
class FeatureTypeEditMappingAdmin(admin.ModelAdmin):
list_display = ("name", "edit_name")


@admin.register(IconDrawingConfig)
class IconDrawingInfoAdmin(admin.ModelAdmin):
list_display = ("name", "image_type", "png_size", "active")
25 changes: 25 additions & 0 deletions map/migrations/0008_icondrawingconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.20 on 2025-05-19 09:59

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
('map', '0007_layer_clustered'),
]

operations = [
migrations.CreateModel(
name='IconDrawingConfig',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=200, unique=True, verbose_name='Name')),
('active', models.BooleanField(default=False, verbose_name='Active')),
('image_type', models.CharField(choices=[('png', 'png'), ('svg', 'svg')], max_length=10, verbose_name='Image Type')),
('png_size', models.IntegerField(choices=[(32, 'Small'), (64, 'Medium'), (128, 'Large'), (256, 'Extra Large')], verbose_name='PNG Size')),
('scale', models.FloatField(default=None, null=True, verbose_name='Scale')),
],
),
]
44 changes: 43 additions & 1 deletion map/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid

from django.db import models
from django.db import models, NotSupportedError
from django.utils.translation import gettext_lazy as _


Expand Down Expand Up @@ -43,3 +43,45 @@ class FeatureTypeEditMapping(models.Model):
@staticmethod
def get_featuretype_edit_name_mapping():
return {mapping.name: mapping.edit_name for mapping in FeatureTypeEditMapping.objects.all()}


class IconDrawingConfig(models.Model):
DEFAULT_ICON_URL = "traffic_control/svg/traffic_sign_icons/"
DEFAULT_ICON_SCALE = 0.075

class ImageType(models.TextChoices):
PNG = "png", _("png")
SVG = "svg", _("svg")

class PngSize(models.IntegerChoices):
SMALL = 32
MEDIUM = 64
LARGE = 128
EXTRA_LARGE = 256

id = models.UUIDField(primary_key=True, unique=True, editable=False, default=uuid.uuid4)
name = models.CharField(_("Name"), max_length=200, unique=True)
active = models.BooleanField(_("Active"), default=False)
image_type = models.CharField(_("Image Type"), choices=ImageType.choices, max_length=10, null=False, blank=False)
png_size = models.IntegerField(_("PNG Size"), choices=PngSize.choices, null=False, blank=False)
scale = models.FloatField(_("Scale"), null=True, blank=False, default=None)

constraints = [
models.UniqueConstraint(
fields=["image_type", "png_size"],
name="%(app_label)s_%(class)s_unique_image_type_png_size",
),
models.UniqueConstraint(
fields=["active"],
condition=models.Q(active=True),
name="%(app_label)s_%(class)s_unique_active",
),
]

@property
def icons_relative_url(self) -> str:
if self.image_type == IconDrawingConfig.ImageType.SVG:
return self.DEFAULT_ICON_URL
elif self.image_type == IconDrawingConfig.ImageType.PNG:
return f"traffic_control/png/traffic_sign_icons/{self.png_size}/"
raise NotSupportedError(f"No support for image type: {self.image_type}")
15 changes: 15 additions & 0 deletions map/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import factory.django

from map.models import IconDrawingConfig


class IconDrawingConfigFactory(factory.django.DjangoModelFactory):
class Meta:
model = IconDrawingConfig
django_get_or_create = ("name",)

name = factory.Sequence(lambda n: f"iconDrawConfig{n}")
active = False
image_type = "svg"
png_size = 32
scale = IconDrawingConfig.DEFAULT_ICON_SCALE
35 changes: 34 additions & 1 deletion map/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from django.test import RequestFactory, TestCase
from django.urls import reverse

from map.models import FeatureTypeEditMapping, Layer
from map.models import FeatureTypeEditMapping, IconDrawingConfig, Layer
from map.tests.factories import IconDrawingConfigFactory
from map.views import map_config, map_view
from traffic_control.tests.factories import get_user

Expand Down Expand Up @@ -32,6 +33,38 @@ class MapConfigTestCase(TestCase):
def setUp(self):
self.factory = RequestFactory()

def test_with_no_icon_draw_config(self):
"""Test that without any active IconDrawingConfig the default values are used."""
request = self.factory.get(reverse("map-config"))
request.LANGUAGE_CODE = "en"

response = map_config(request)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response_data["icon_scale"], IconDrawingConfig.DEFAULT_ICON_SCALE)
self.assertEqual(response_data["icon_type"], "svg")
self.assertEqual(
response_data["traffic_sign_icons_url"],
f"{request.build_absolute_uri(settings.STATIC_URL)}traffic_control/svg/traffic_sign_icons/",
)

def test_with_icon_draw_config(self):
"""Test that with an active IconDrawingConfig the values are actually used."""
idc = IconDrawingConfigFactory(active=True, scale=IconDrawingConfig.DEFAULT_ICON_SCALE + 0.1, image_type="png")
request = self.factory.get(reverse("map-config"))
request.LANGUAGE_CODE = "en"

response = map_config(request)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)

self.assertEqual(response_data["icon_scale"], idc.scale)
self.assertEqual(response_data["icon_type"], idc.image_type)
self.assertEqual(
response_data["traffic_sign_icons_url"],
f"{request.build_absolute_uri(settings.STATIC_URL)}traffic_control/png/traffic_sign_icons/{idc.png_size}/",
)

def test_layer_config_return_ok(self):
Layer.objects.create(
identifier="basemap",
Expand Down
6 changes: 5 additions & 1 deletion map/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from django.urls import reverse
from django.utils.translation import gettext as _

from traffic_control.services.icon_draw_config import get_icons_relative_url, get_icons_scale, get_icons_type

from .models import FeatureTypeEditMapping, Layer


Expand Down Expand Up @@ -37,7 +39,7 @@ def map_config(request):
}
)

traffic_sign_icons_url = f"{request.build_absolute_uri(settings.STATIC_URL)}traffic_control/svg/traffic_sign_icons/"
traffic_sign_icons_url = f"{request.build_absolute_uri(settings.STATIC_URL)}{get_icons_relative_url()}"
config = {
"basemapConfig": {
"name": _("Basemaps"),
Expand All @@ -55,6 +57,8 @@ def map_config(request):
"imageExtent": _get_overview_image_extent(),
},
"traffic_sign_icons_url": traffic_sign_icons_url,
"icon_scale": get_icons_scale(),
"icon_type": get_icons_type(),
"featureTypeEditNameMapping": FeatureTypeEditMapping.get_featuretype_edit_name_mapping(),
}
return JsonResponse(config)
Expand Down
25 changes: 25 additions & 0 deletions traffic_control/services/icon_draw_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.db import models

from map.models import IconDrawingConfig


def get_active_icon_drawing_config():
try:
return IconDrawingConfig.objects.get(active=True)
except models.ObjectDoesNotExist:
return None


def get_icons_relative_url():
config = get_active_icon_drawing_config()
return get_active_icon_drawing_config().icons_relative_url if config else IconDrawingConfig.DEFAULT_ICON_URL


def get_icons_scale():
config = get_active_icon_drawing_config()
return get_active_icon_drawing_config().scale if config else IconDrawingConfig.DEFAULT_ICON_SCALE


def get_icons_type():
config = get_active_icon_drawing_config()
return get_active_icon_drawing_config().image_type if config else "svg"
Loading