Skip to content

Commit

Permalink
Merge pull request #102 from shannonturner/v7.1
Browse files Browse the repository at this point in the history
V7.1 - Eyedropper, Move Station, London, and 8 new station styles
  • Loading branch information
shannonturner authored Jan 13, 2025
2 parents ac104a9 + d4bae97 commit 0d666d4
Show file tree
Hide file tree
Showing 29 changed files with 2,151 additions and 466 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ All Maps Ever

Browse all of the 200,000 maps (and counting) in the library: [https://metromapmaker.com/calendar/](https://metromapmaker.com/calendar/)

Development log
----------------

See sneak previews, how development is progressing, when you can expect new releases, what new features I'm prioritizing and what I want to build next plus so much more at [blog.metromapmaker.com/](https://blog.metromapmaker.com/)

Made something cool?
---------------------
I'd love to see it! [Say hi!](mailto:[email protected]?subject=Metro+Map+Maker)
Expand Down
22 changes: 22 additions & 0 deletions metro_map_saver/map_saver/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,25 @@ class Meta:
'name',
'map_type',
]

class CustomListForm(forms.Form):
maps = forms.CharField(
widget=forms.Textarea(
attrs={
'rows': 10,
'class': 'w-100',
'placeholder': '''225,007
215094
wxtIWRy8
metromapmaker.com/map/XI9PhazG
''',
},
),

)

def clean_maps(self):
maps = self.cleaned_data['maps'] or ''
maps = maps.strip()
maps = maps.replace(',', '').replace('#', '')
return maps
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from django.core.management.base import BaseCommand
from map_saver.models import SavedMap

class Command(BaseCommand):
help = """
One-off script to fix the ~300 map names that have ampersands
corrupted to &
"""

def add_arguments(self, parser):
parser.add_argument(
'-l',
'--limit',
type=int,
dest='limit',
default=1000,
help='Only process this many maps at once.',
)

parser.add_argument(
'--dry-run',
action='store_true',
dest='dry_run',
default=False,
help='Dry run to show which maps and replacements would occur, but do not actually change any maps.',
)

def handle(self, *args, **kwargs):
limit = kwargs['limit']
dry_run = kwargs.get('dry_run')
needs_editing = SavedMap.objects.filter(name__contains='&')
count = needs_editing.count()

replacements = [
'&' + ('amp;' * 5),
'&' + ('amp;' * 4),
'&' + ('amp;' * 3),
'&' + ('amp;' * 2),
'&' + ('amp;'),
]

for mmap in needs_editing[:limit]:

new_name = mmap.name
for replacement in replacements:
new_name = new_name.replace(replacement, '&')

if dry_run:
print(f'Would rename {mmap.id} (from {mmap.name} to {new_name})')
else:
mmap.name = new_name
mmap.save()

self.stdout.write(f'{"Would replace" if dry_run else "Replaced"} & to ampersands in the name for {min(count, limit)} maps.')
82 changes: 75 additions & 7 deletions metro_map_saver/map_saver/management/commands/replace_mapdata.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.core.management.base import BaseCommand
from map_saver.models import SavedMap
import json

class Command(BaseCommand):
help = """
Expand All @@ -18,22 +20,88 @@ class Command(BaseCommand):
Usage: manage.py replace_mapdata <find> <replace>
Example: manage.py replace_mapdata "&#x2f;" "-"
2024 Nov replacements:
&amp;#x27; '
&amp;#x2f; /
&amp;amp; &
&amp; &
--- then ---
&#x2f; /
&#x27; '
Use with caution: this could break or otherwise mangle your maps.
"""

def add_arguments(self, parser):
parser.add_argument('find', type=str, help='String to search for in the mapdata')
parser.add_argument('replace', type=str, help='String to replace it with in the mapdata')
parser.add_argument(
'find',
type=str,
help='String to search for in the data (v2+)',
)

def handle(self, *args, **kwargs):
from map_saver.models import SavedMap
parser.add_argument(
'replace',
type=str,
help='String to replace it with in the data (v2+)',
)

parser.add_argument(
'-l',
'--limit',
type=int,
dest='limit',
default=1000,
help='Only process this many maps at once.',
)

parser.add_argument(
'--amp',
action='store_true',
dest='amp',
default=False,
help='Replace all numbers of &amp;amp;'
)

parser.add_argument(
'--dry-run',
action='store_true',
dest='dry_run',
default=False,
help='Dry run to show which maps would be affected.',
)

def handle(self, *args, **kwargs):
limit = kwargs['limit']
dry_run = kwargs.get('dry_run')
find = kwargs.get('find')
replace = kwargs.get('replace')
amp = kwargs.get('amp')

amp_replacements = [
'&' + ('amp;' * x)
for x in range(60, 0, -1) # A little absurd, but the upper limit of what's allowed
]

saved_maps = SavedMap.objects.raw("SELECT * FROM map_saver_savedmap WHERE LOCATE(%s, data) ORDER BY id", [find])
self.stdout.write(f"Found {len(saved_maps)} maps that contained the criteria: {find} -- replacement is: {replace}")

ids_and_hashes = []
for mmap in saved_maps[:limit]:
self.stdout.write(f'[MMM-BACKUP] [#{mmap.id}] [{mmap.urlhash}]: {json.dumps(mmap.data)}')

saved_maps = SavedMap.objects.filter(mapdata__contains=find)
count = saved_maps.count()
self.stdout.write(f"Found {count} maps that contained the criteria: {find}")
new_data = json.dumps(mmap.data)
if amp:
for amp_replacement in amp_replacements:
new_data = new_data.replace(amp_replacement, replace)
else:
new_data = new_data.replace(find, replace)

self.stdout.write(f'\t#{mmap.id}: {new_data}')
ids_and_hashes.append(f'{mmap.id},{mmap.urlhash}')

if not dry_run:
mmap.data = json.loads(new_data)
mmap.save()

self.stdout.write(f'Processed the following {len(ids_and_hashes)} maps, worth a spot-check:')
self.stdout.write('\n'.join(ids_and_hashes))
34 changes: 32 additions & 2 deletions metro_map_saver/map_saver/mapdata_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .validator import VALID_XY, ALLOWED_MAP_SIZES, ALLOWED_ORIENTATIONS

# For use with data version 2
SVG_TEMPLATE = Template('''
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {{ canvas_size|default:80 }} {{ canvas_size|default:80 }}">
{% spaceless %}
Expand Down Expand Up @@ -41,12 +42,41 @@
{% get_station_styles_in_use stations default_station_shape line_size %}
{% else %}
<style>line { stroke-width: {{ line_size|default:1 }}; fill: none; stroke-linecap: round; stroke-linejoin: round; }{% for hex, class_name in color_map.items %} .{{ class_name }} { stroke: #{{ hex }} }{% endfor %} {% get_line_width_styles_for_svg_style shapes_by_color %}</style>
{% endif %}
{% if shapes_by_color|has_line_style:"color_outline" %}
<filter id="fco" filterUnits="userSpaceOnUse">
<feBlend in="SourceGraphic" in2="SourceGraphic" mode="screen"/>
</filter>
{% endif %}
{% for color, line_width_style in shapes_by_color.items %}
{% for width_style, shapes in line_width_style.items %}
{% with line_style=width_style|get_style %}
{% for line in shapes.lines %}
<line class="{% map_color color color_map %} {% get_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}"/>
{% if 'hollow' in line_style or line_style == 'color_outline' %}
<mask id="k{{ forloop.parentloop.parentloop.counter }}-{{ forloop.parentloop.counter }}-{{ forloop.counter }}" maskUnits="userSpaceOnUse">
<line class="{% get_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}" stroke="#fff"/>
{% if 'hollow' in line_style or line_style == 'color_outline' %}
<line class="{% get_masked_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}" stroke="#000"/>
{% endif %}
</mask>
{% if line_style == 'color_outline' %}
<line class="{% map_color color color_map %} {% get_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}" filter="url(#fco)"/>
{% endif %}
<line class="{% map_color color color_map %} {% get_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}" mask="url(#k{{ forloop.parentloop.parentloop.counter }}-{{ forloop.parentloop.counter }}-{{ forloop.counter }})"/>
{% elif 'stripes' in line_style %}
<mask id="k{{ forloop.parentloop.parentloop.counter }}-{{ forloop.parentloop.counter }}-{{ forloop.counter }}" maskUnits="userSpaceOnUse">
<line class="{% get_line_class_from_width_style width_style line_size True %} {% if line_style == 'wide_stripes' %}sl-sq{% else %}sl-b{% endif %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}" stroke="#fff"/>
<line class="{% get_masked_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}" stroke="#000"/>
</mask>
<line class="{% map_color color color_map %} {% get_line_class_from_width_style width_style line_size True %} {% if line_style == 'wide_stripes' %}sl-sq{% else %}sl-b{% endif %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}" mask="url(#k{{ forloop.parentloop.parentloop.counter }}-{{ forloop.parentloop.counter }}-{{ forloop.counter }})"/>
<line class="{% map_color color color_map %} {% get_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}"/>
{% elif line_style == 'dotted_square' %}
<line class="{% map_color color color_map %} {% get_line_class_from_width_style width_style line_size %} {% get_masked_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}"/>
{% else %}
<line class="{% map_color color color_map %} {% get_line_class_from_width_style width_style line_size %}" x1="{{ line.0 }}" y1="{{ line.1 }}" x2="{{ line.2 }}" y2="{{ line.3 }}"/>
{% endif %}
{% endfor %}
{% endwith %} {# line_style #}
{% for point in shapes.points %}
{% if default_station_shape == 'rect' %}
<rect x="{{ point.0|add:-0.5 }}" y="{{ point.1|add:-0.5 }}" w="1" h="1" fill="#{{ color }}" />
Expand All @@ -65,7 +95,7 @@
{% load metromap_utils %}
{% for station in stations %}
{% station_marker station default_station_shape line_size points_by_color stations data_version %}
{% station_text station %}
{% station_text station points_by_color %} {# pbc needed because london-style stations want line direction #}
{% endfor %}
{% endspaceless %}
</svg>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.1.2 on 2024-11-28 03:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('map_saver', '0032_city_map_saver_c_name_f3a223_idx'),
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
]

operations = [
migrations.AddField(
model_name='savedmap',
name='browse_visible',
field=models.BooleanField(default=True, help_text='Uncheck this to disable a map from being able to be browsed in the maps by date and similar views, though it is still accessible by direct link.'),
),
migrations.AlterField(
model_name='savedmap',
name='gallery_visible',
field=models.BooleanField(default=True, help_text='Should this be shown in the default view of the Admin Gallery?'),
),
migrations.AddIndex(
model_name='savedmap',
index=models.Index(fields=['browse_visible'], name='map_saver_s_browse__add01f_idx'),
),
]
5 changes: 4 additions & 1 deletion metro_map_saver/map_saver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ class SavedMap(models.Model):
# v2+ representation of map data
data = models.JSONField(default=dict, blank=True)
# gallery_visible: should this be shown in the default view of the Admin Gallery?
gallery_visible = models.BooleanField(default=True)
# Essentially -- which maps have I already reviewed?
gallery_visible = models.BooleanField(default=True, help_text='Should this be shown in the default view of the Admin Gallery?')
# publicly_visible: should this be shown in the publicly-visible gallery?
# (using this to improve speed and reduce query complexity)
publicly_visible = models.BooleanField(default=False)
browse_visible = models.BooleanField(default=True, help_text='Uncheck this to disable a map from being able to be browsed in the maps by date and similar views, though it is still accessible by direct link.')
name = models.CharField(max_length=255, blank=True, default='', help_text='User-provided (or admin-provided) name for a map. When user-provided, contains tags like (real).')
thumbnail = models.TextField(blank=True, default='') # Consider: Delete after thumbnail files generation migration
thumbnail_svg = models.FileField(upload_to=get_thumbnail_filepath, null=True, blank=True)
Expand Down Expand Up @@ -354,6 +356,7 @@ class Meta:
models.Index(fields=["urlhash"]),
models.Index(fields=["gallery_visible"]),
models.Index(fields=["publicly_visible"]),
models.Index(fields=["browse_visible"]),
models.Index(fields=["created_at"]),
models.Index(fields=["station_count"]),
models.Index(fields=["name"]),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 0d666d4

Please sign in to comment.