From 8c774fb7b36669e0740ffd5706d7e023ca45d97d Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 30 Oct 2023 21:07:55 +0100 Subject: [PATCH 1/4] Add Pictogram.category and list pictos grouped by category --- umap/migrations/0016_pictogram_category.py | 17 ++++++++++ umap/models.py | 2 ++ umap/static/umap/base.css | 18 +++++----- umap/static/umap/js/umap.core.js | 26 +++++++-------- umap/static/umap/js/umap.forms.js | 39 +++++++++++++++++----- 5 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 umap/migrations/0016_pictogram_category.py diff --git a/umap/migrations/0016_pictogram_category.py b/umap/migrations/0016_pictogram_category.py new file mode 100644 index 00000000..88db888c --- /dev/null +++ b/umap/migrations/0016_pictogram_category.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2023-10-30 17:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("umap", "0015_alter_pictogram_pictogram"), + ] + + operations = [ + migrations.AddField( + model_name="pictogram", + name="category", + field=models.CharField(blank=True, max_length=300, null=True), + ), + ] diff --git a/umap/models.py b/umap/models.py index 40588399..cc676602 100644 --- a/umap/models.py +++ b/umap/models.py @@ -284,6 +284,7 @@ class Pictogram(NamedModel): """ attribution = models.CharField(max_length=300) + category = models.CharField(max_length=300, null=True, blank=True) pictogram = models.FileField(upload_to="pictogram") @property @@ -292,6 +293,7 @@ class Pictogram(NamedModel): "id": self.pk, "attribution": self.attribution, "name": self.name, + "category": self.category, "src": self.pictogram.url, } diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index e92d39fd..5152f4ce 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -521,16 +521,14 @@ i.info { margin-top: -8px; padding: 0 5px; } -.umap-icon-list, .umap-pictogram-list { - clear: both; +.umap-pictogram-grid { + display: flex; + flex-wrap: wrap; } -.umap-icon-choice { - display: block; - float: left; +.umap-pictogram-choice { width: 30px; height: 30px; line-height: 30px; - position: relative; cursor: pointer; background-image: url('./img/icon-bg.png'); text-align: center; @@ -538,16 +536,16 @@ i.info { margin-bottom: 5px; margin-right: 5px; } -.umap-icon-choice img { +.umap-pictogram-choice img { vertical-align: middle; max-width: 24px; } -.umap-icon-choice:hover, -.umap-icon-choice.selected, +.umap-pictogram-choice:hover, +.umap-pictogram-choice.selected, .umap-color-picker span:hover { box-shadow: 0 0 4px 0 black; } -.umap-icon-choice .leaflet-marker-icon { +.umap-pictogram-choice .leaflet-marker-icon { bottom: 0; left: 30px; position: absolute; diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index cf62745e..f392322a 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -201,6 +201,16 @@ L.Util.greedyTemplate = (str, data, ignore) => { ) } +L.Util.naturalSort = (a, b) => { + return a + .toString() + .toLowerCase() + .localeCompare(b.toString().toLowerCase(), L.lang || 'en', { + sensitivity: 'base', + numeric: true, + }) +} + L.Util.sortFeatures = (features, sortKey) => { const sortKeys = (sortKey || 'name').split(',') @@ -214,19 +224,9 @@ L.Util.sortFeatures = (features, sortKey) => { let score const valA = a.properties[sortKey] || '' const valB = b.properties[sortKey] || '' - if (!valA) { - score = -1 - } else if (!valB) { - score = 1 - } else { - score = valA - .toString() - .toLowerCase() - .localeCompare(valB.toString().toLowerCase(), L.lang || 'en', { - sensitivity: 'base', - numeric: true, - }) - } + if (!valA) score = -1 + else if (!valB) score = 1 + else score = L.Util.naturalSort(valA, valB) if (score === 0 && sortKeys[i + 1]) return sort(a, b, i + 1) return score * reverse } diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index d5411ef5..20cd99b3 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -547,7 +547,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const img = L.DomUtil.create( 'img', '', - L.DomUtil.create('div', 'umap-icon-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) img.src = this.value() L.DomEvent.on(img, 'click', this.fetchIconList, this) @@ -555,7 +555,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ const el = L.DomUtil.create( 'span', '', - L.DomUtil.create('div', 'umap-icon-choice', this.buttonsContainer) + L.DomUtil.create('div', 'umap-pictogram-choice', this.buttonsContainer) ) el.textContent = this.value() L.DomEvent.on(el, 'click', this.fetchIconList, this) @@ -570,11 +570,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ ) }, - addIconPreview: function (pictogram) { - const baseClass = 'umap-icon-choice', + addIconPreview: function (pictogram, parent) { + const baseClass = 'umap-pictogram-choice', value = pictogram.src, className = value === this.value() ? `${baseClass} selected` : baseClass, - container = L.DomUtil.create('div', className, this.pictogramsContainer), + container = L.DomUtil.create('div', className, parent), img = L.DomUtil.create('img', '', container) img.src = value if (pictogram.name && pictogram.attribution) { @@ -602,7 +602,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }, search: function (e) { - const icons = [...this.parentNode.querySelectorAll('.umap-icon-choice')], + const icons = [...this.parentNode.querySelectorAll('.umap-pictogram-choice')], search = this.searchInput.value.toLowerCase() icons.forEach((el) => { if (el.title.toLowerCase().indexOf(search) != -1) el.style.display = 'block' @@ -610,13 +610,36 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ }) }, + addCategory: function (category, items) { + const parent = L.DomUtil.create( + 'div', + 'umap-pictogram-category', + this.pictogramsContainer + ), + title = L.DomUtil.add('h6', '', parent, category), + grid = L.DomUtil.create('div', 'umap-pictogram-grid', parent) + for (let item of items) { + this.addIconPreview(item, grid) + } + }, + buildIconList: function (data) { this.searchInput = L.DomUtil.create('input', '', this.pictogramsContainer) this.searchInput.type = 'search' this.searchInput.placeholder = L._('Search') L.DomEvent.on(this.searchInput, 'input', this.search, this) - for (const idx in data.pictogram_list) { - this.addIconPreview(data.pictogram_list[idx]) + const categories = {} + let category + for (const props of data.pictogram_list) { + category = props.category || L._('Generic') + categories[category] = categories[category] || [] + categories[category].push(props) + } + const sorted = Object.entries(categories).toSorted(([a], [b]) => + L.Util.naturalSort(a, b) + ) + for (let [category, items] of sorted) { + this.addCategory(category, items) } const closeButton = L.DomUtil.createButton( 'button action-button', From f61f1415ce0556c085aeb79cfa35675d30eef2b7 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 30 Oct 2023 21:24:26 +0100 Subject: [PATCH 2/4] Update pictograms import script to deal with category and SVG --- docs/administration.md | 37 ++++----- umap/management/commands/import_pictograms.py | 77 ++++++++++++------- 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/docs/administration.md b/docs/administration.md index 51502752..8b702a0d 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -10,11 +10,17 @@ Icons (aka pictograms in uMap sources) can be used in your map markers. Icons are not embedded in uMap sources, you will have to add them manually. So you can choose which icons you want to use. +You can use either PNG, JPG or SVG files. SVG files are recommanded. + +When using SVG, it's recommanded to use icons without color. UMap will switch to white colors +automatically according to the marker background color. + Example of icons libraries you may want to use: - [Maki Icons](https://labs.mapbox.com/maki-icons/) (icon set made for map designers) - [Osmic Icons](https://gitlab.com/gmgeo/osmic) - [SJJB Icons](http://www.sjjb.co.uk/mapicons/contactsheet) +- [Remix](https://remixicon.com/) ### Import icons manually @@ -22,32 +28,15 @@ You can import icons manually by going to your uMap admin page: `https://your.se ### Import icons automatically -To import icons on your uMap server, you will need to use command `umap import_pictograms` +To import icons on your uMap server, you will need to use command `umap import_pictograms`. -Note, you can get help with `umap import_pictograms -h` +Note: you can get help with `umap import_pictograms -h` -In this example, we will import Maki icons. +Basic usage: -First, we download icons from main site. Inside the downloaded archive, we keep only the icons folder that contains svg files. Place this folder on your server. + umap import_pictograms --attribution "Maki Icons by Mapbox" path/to/icons/directory/ -Go inside icons folder and remove tiny icons: `rm *-11.svg` +### Categories -Now, we will use imagemagick to convert svg to png. - -`for file in *.svg; do convert -background none $file ${file%15.svg}24.png; done` - -To have white icons use: -`for file in *.svg; do convert -background none -fuzz 100% -fill white -opaque black $file ${file%15.svg}24.png; done` - - -Notes: -- you may also want to resize image with option `-resize 24x` -- this solution is not optimal, generated png are blurry. - -This will convert the svg to png and rename them from `*-15.svg` to `*-24.png` - -Now we will import icons. Note: icons names must end with `-24.png` - -`umap import_pictograms --attribution "Maki Icons by Mapbox" icons` - -Done. Icons are imported. +uMap can render icons grouped into categories. When using the import script, any +subfolder will be used as category. diff --git a/umap/management/commands/import_pictograms.py b/umap/management/commands/import_pictograms.py index 80cd6799..caa1ceb4 100644 --- a/umap/management/commands/import_pictograms.py +++ b/umap/management/commands/import_pictograms.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path from django.core.files import File from django.core.management.base import BaseCommand @@ -7,40 +7,61 @@ from umap.models import Pictogram class Command(BaseCommand): - help = 'Import pictograms from a folder' + help = "Import pictograms from a folder" def add_arguments(self, parser): - parser.add_argument('path') - parser.add_argument('--attribution', required=True, - help='Attribution of the imported pictograms') - parser.add_argument('--suffix', - help='Optional suffix to add to each name') - parser.add_argument('--force', action='store_true', - help='Update picto if it already exists.') + parser.add_argument("path") + parser.add_argument( + "--attribution", + required=True, + help="Attribution of the imported pictograms", + ) + parser.add_argument( + "--extensions", + help="Optional list of extensins to process", + nargs="+", + default=[".svg"], + ) + parser.add_argument( + "--exclude", + help="Optional list of files or dirs to exclude", + nargs="+", + default=["font"], + ) + parser.add_argument( + "--force", action="store_true", help="Update picto if it already exists." + ) def handle(self, *args, **options): - path = options['path'] - attribution = options['attribution'] - suffix = options['suffix'] - force = options['force'] - for filename in os.listdir(path): - if filename.endswith("-24.png"): - name = self.extract_name(filename) - if suffix: - name = '{name}{suffix}'.format(name=name, suffix=suffix) + self.path = Path(options["path"]) + self.attribution = options["attribution"] + self.extensions = options["extensions"] + self.force = options["force"] + self.exclude = options["exclude"] + self.handle_directory(self.path) + + def handle_directory(self, path): + for filename in path.iterdir(): + if filename.name in self.exclude: + continue + if filename.is_dir(): + self.handle_directory(filename) + continue + if filename.suffix in self.extensions: + name = filename.stem picto = Pictogram.objects.filter(name=name).last() if picto: - if not force: - self.stdout.write(u"⚠ Pictogram with name '{name}' already exists. Skipping.".format(name=name)) # noqa + if not self.force: + self.stdout.write( + f"⚠ Pictogram with name '{name}' already exists. Skipping." + ) continue else: picto = Pictogram() picto.name = name - filepath = os.path.join(path, filename) - with open(filepath, 'rb') as f: - picto.attribution = attribution - picto.pictogram.save(filename, File(f), save=True) - self.stdout.write(u"✔ Imported pictogram {filename}.".format(filename=filename)) # noqa - - def extract_name(self, filename): - return filename[:-7].replace('-', ' ') + if (path.name != self.path.name): # Subfolders only + picto.category = path.name + picto.attribution = self.attribution + with (path / filename).open("rb") as f: + picto.pictogram.save(filename.name, File(f), save=True) + self.stdout.write(f"✔ Imported pictogram {filename}.") From 15eea6ead9380d5833065db1309ade80a4e61a78 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 30 Oct 2023 21:51:05 +0100 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: David Larlet <3556+davidbgk@users.noreply.github.com> --- docs/administration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/administration.md b/docs/administration.md index 8b702a0d..5c1b9c03 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -10,9 +10,9 @@ Icons (aka pictograms in uMap sources) can be used in your map markers. Icons are not embedded in uMap sources, you will have to add them manually. So you can choose which icons you want to use. -You can use either PNG, JPG or SVG files. SVG files are recommanded. +You can use either PNG, JPG or SVG files. SVG files are recommended. -When using SVG, it's recommanded to use icons without color. UMap will switch to white colors +When using SVG, it's recommended to use icons without color. uMap will switch to white colors automatically according to the marker background color. Example of icons libraries you may want to use: From de8bbc285182815c208d009d46ccbeecf2dc05a7 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 30 Oct 2023 22:28:15 +0100 Subject: [PATCH 4/4] Update docs/administration.md Co-authored-by: David Larlet <3556+davidbgk@users.noreply.github.com> --- docs/administration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration.md b/docs/administration.md index 5c1b9c03..84488613 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -28,7 +28,7 @@ You can import icons manually by going to your uMap admin page: `https://your.se ### Import icons automatically -To import icons on your uMap server, you will need to use command `umap import_pictograms`. +To import icons on your uMap server, you will need to use the command `umap import_pictograms`. Note: you can get help with `umap import_pictograms -h`