diff --git a/metro_map_saver/map_saver/forms.py b/metro_map_saver/map_saver/forms.py index 2c8fc6ca..a12776fb 100644 --- a/metro_map_saver/map_saver/forms.py +++ b/metro_map_saver/map_saver/forms.py @@ -5,6 +5,7 @@ hex64, validate_metro_map, validate_metro_map_v2, + validate_metro_map_v3, ) import hashlib @@ -24,7 +25,10 @@ def clean_mapdata(self): mapdata = self.cleaned_data['mapdata'] data_version = mapdata.get('global', {}).get('data_version', 1) - if data_version == 2: + if data_version == 3: + mapdata = validate_metro_map_v3(mapdata) + mapdata['global']['data_version'] = 3 + elif data_version == 2: mapdata = validate_metro_map_v2(mapdata) mapdata['global']['data_version'] = 2 else: diff --git a/metro_map_saver/map_saver/mapdata_optimizer.py b/metro_map_saver/map_saver/mapdata_optimizer.py index 20304529..04b39233 100644 --- a/metro_map_saver/map_saver/mapdata_optimizer.py +++ b/metro_map_saver/map_saver/mapdata_optimizer.py @@ -31,11 +31,40 @@ ''') +# For use with data_version >= 3 +SVG_TEMPLATE_V3 = Template(''' + +{% spaceless %} +{% load metromap_utils %} +{% if stations %} + + {% get_station_styles_in_use stations default_station_shape line_size %} +{% else %} + +{% endif %} + {% for color, line_width_style in shapes_by_color.items %} + {% for width_style, shapes in line_width_style.items %} + {% for line in shapes.lines %} + + {% endfor %} + {% for point in shapes.points %} + {% if default_station_shape == 'rect' %} + + {% else %} + + {% endif %} + {% endfor %} + {% endfor %} + {% endfor %} +{% endspaceless %} + +''') + STATIONS_SVG_TEMPLATE = Template(''' {% spaceless %} {% load metromap_utils %} {% for station in stations %} - {% station_marker station default_station_shape line_size points_by_color stations %} + {% station_marker station default_station_shape line_size points_by_color stations data_version %} {% station_text station %} {% endfor %} {% endspaceless %} @@ -73,10 +102,13 @@ def sort_points_by_color(mapdata, map_type='classic', data_version=1): # Order matters 1: reversed(ALLOWED_MAP_SIZES), 2: reversed(ALLOWED_MAP_SIZES), + 3: reversed(ALLOWED_MAP_SIZES), } } allowed_sizes = allowed_sizes[map_type][data_version] + linewidthstyles_by_xy = {} + if map_type == 'classic' and data_version == 1: # Ex: [0][1]['line']: 'bd1038' for x in sorted(mapdata): @@ -163,6 +195,49 @@ def sort_points_by_color(mapdata, map_type='classic', data_version=1): points_by_color[line_color]['xy'].add((x, y)) + elif map_type == 'classic' and data_version == 3: + colors_by_xy = {} + + default_station_shape = mapdata['global'].get('style', {}).get('mapStationStyle', 'wmata') + + for line_color in mapdata['points_by_color']: + for width_style in mapdata['points_by_color'][line_color]: + for x in mapdata['points_by_color'][line_color][width_style]: + for y in mapdata['points_by_color'][line_color][width_style][x]: + x = str(x) + y = str(y) + if x not in VALID_XY or y not in VALID_XY: + continue + + if mapdata['points_by_color'][line_color][width_style][x][y] != 1: + continue + + if line_color not in points_by_color: + points_by_color[line_color] = { + width_style: set(), + } + elif width_style not in points_by_color[line_color]: + points_by_color[line_color][width_style] = set() + + colors_by_xy[f'{x},{y}'] = line_color + linewidthstyles_by_xy[f'{x},{y}'] = width_style + + x = int(x) + y = int(y) + + if x > highest_seen: + highest_seen = x + if y > highest_seen: + highest_seen = y + + points_by_color[line_color][width_style].add((x, y)) + + if map_type == 'classic' and data_version >= 2: + + default_line_width = mapdata['global'].get('style', {}).get('mapLineWidth', 1) + default_line_style = mapdata['global'].get('style', {}).get('mapLineStyle', 'solid') + default_line_width_style = f'{default_line_width}-{default_line_style}' + for x in mapdata['stations']: for y in mapdata['stations'][x]: if x not in VALID_XY or y not in VALID_XY: @@ -174,12 +249,14 @@ def sort_points_by_color(mapdata, map_type='classic', data_version=1): 'orientation': station.get('orientation', 0), 'xy': (int(x), int(y)), 'color': colors_by_xy[f'{x},{y}'], + 'line_width_style': linewidthstyles_by_xy.get(f'{x},{y}', default_line_width_style) } if station.get('transfer'): station_data['transfer'] = 1 station_data['style'] = station.get('style', default_station_shape) stations.append(station_data) + for size in allowed_sizes: if highest_seen < size: map_size = size @@ -342,7 +419,7 @@ def reduce_straight_line(line): # Can't be reduced further return line -def get_svg_from_shapes_by_color(shapes_by_color, map_size, line_size, default_station_shape, points_by_color, stations=False): +def get_svg_from_shapes_by_color(shapes_by_color, map_size, line_size, default_station_shape, points_by_color, stations=False, data_version=3): """ Finally, let's draw SVG from the sorted shapes by color. @@ -365,9 +442,12 @@ def get_svg_from_shapes_by_color(shapes_by_color, map_size, line_size, default_s 'color_map': {color: f'c{index}' for index, color in enumerate(points_by_color.keys())}, } - return SVG_TEMPLATE.render(Context(context)) + if data_version >= 3: + return SVG_TEMPLATE_V3.render(Context(context)) + else: + return SVG_TEMPLATE.render(Context(context)) -def add_stations_to_svg(thumbnail_svg, line_size, default_station_shape, points_by_color, stations): +def add_stations_to_svg(thumbnail_svg, line_size, default_station_shape, points_by_color, stations, data_version): """ This allows me to avoid generating the map SVG twice (once for thumbnails, once for stations) @@ -383,6 +463,7 @@ def add_stations_to_svg(thumbnail_svg, line_size, default_station_shape, points_ 'default_station_shape': default_station_shape, 'points_by_color': points_by_color, 'stations': stations, + "data_version": data_version, } return thumbnail_svg.replace('', STATIONS_SVG_TEMPLATE.render(Context(context))) diff --git a/metro_map_saver/map_saver/models.py b/metro_map_saver/map_saver/models.py index b941d255..9440ee3e 100644 --- a/metro_map_saver/map_saver/models.py +++ b/metro_map_saver/map_saver/models.py @@ -145,10 +145,10 @@ def _station_count(self): return 0 @staticmethod - def get_stations(data=None, data_version=2): + def get_stations(data=None, data_version=3): stations = set() - if data_version == 2 and data: + if data_version >= 2 and data: for x in data.get('stations', {}): for y in data['stations'][x]: stations.add(data['stations'][x][y].get('name', '').lower()) @@ -287,18 +287,26 @@ def generate_images(self): points_by_color, stations, map_size = sort_points_by_color(mapdata, data_version=data_version) shapes_by_color = {} - for color in points_by_color: - points_this_color = points_by_color[color]['xy'] - - lines, singletons = find_lines(points_this_color) - shapes_by_color[color] = {'lines': lines, 'points': singletons} - - thumbnail_svg = get_svg_from_shapes_by_color(shapes_by_color, map_size, line_size, default_station_shape, points_by_color, stations) + if data_version <= 2: + for color in points_by_color: + points_this_color = points_by_color[color]['xy'] + + lines, singletons = find_lines(points_this_color) + shapes_by_color[color] = {'lines': lines, 'points': singletons} + elif data_version >= 3: + for color in points_by_color: + shapes_by_color[color] = {} + for width_style in points_by_color[color]: + points_this_color_width_style = points_by_color[color][width_style] + lines, singletons = find_lines(points_this_color_width_style) + shapes_by_color[color][width_style] = {'lines': lines, 'points': singletons} + + thumbnail_svg = get_svg_from_shapes_by_color(shapes_by_color, map_size, line_size, default_station_shape, points_by_color, stations, data_version) thumbnail_svg_file = ContentFile(thumbnail_svg, name=f"t{self.urlhash}.svg") self.thumbnail_svg = thumbnail_svg_file - svg = add_stations_to_svg(thumbnail_svg, line_size, default_station_shape, points_by_color, stations) + svg = add_stations_to_svg(thumbnail_svg, line_size, default_station_shape, points_by_color, stations, data_version) svg_file = ContentFile(svg, name=f"{self.urlhash}.svg") self.svg = svg_file self.save() diff --git a/metro_map_saver/map_saver/static/assets/icons/arrow-clockwise.svg b/metro_map_saver/map_saver/static/assets/icons/arrow-clockwise.svg new file mode 100644 index 00000000..324d5af1 --- /dev/null +++ b/metro_map_saver/map_saver/static/assets/icons/arrow-clockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/metro_map_saver/map_saver/static/assets/icons/arrow-counterclockwise.svg b/metro_map_saver/map_saver/static/assets/icons/arrow-counterclockwise.svg new file mode 100644 index 00000000..3d9ff62e --- /dev/null +++ b/metro_map_saver/map_saver/static/assets/icons/arrow-counterclockwise.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/metro_map_saver/map_saver/static/assets/icons/mmm-icons.svg b/metro_map_saver/map_saver/static/assets/icons/mmm-icons.svg index 072afc08..209213d6 100644 --- a/metro_map_saver/map_saver/static/assets/icons/mmm-icons.svg +++ b/metro_map_saver/map_saver/static/assets/icons/mmm-icons.svg @@ -155,6 +155,21 @@ + + + + + + + + + + + + + + + diff --git a/metro_map_saver/map_saver/static/assets/icons/ruler.svg b/metro_map_saver/map_saver/static/assets/icons/ruler.svg new file mode 100644 index 00000000..90fb01c7 --- /dev/null +++ b/metro_map_saver/map_saver/static/assets/icons/ruler.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/metro_map_saver/map_saver/static/assets/icons/tags.svg b/metro_map_saver/map_saver/static/assets/icons/tags.svg new file mode 100644 index 00000000..ade5519a --- /dev/null +++ b/metro_map_saver/map_saver/static/assets/icons/tags.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/metro_map_saver/map_saver/static/css/metromapmaker.css b/metro_map_saver/map_saver/static/css/metromapmaker.css index b56df2a3..8e7afbd0 100644 --- a/metro_map_saver/map_saver/static/css/metromapmaker.css +++ b/metro_map_saver/map_saver/static/css/metromapmaker.css @@ -49,6 +49,7 @@ input[type=checkbox]{margin:4px 0 0;margin-top:1px\9;line-height:normal} #controls:not(.collapsed) button:not(.width-100):not(.rail-line) { width: 45%; height: 6rem; + overflow: clip; } #controls:not(.collapsed) #toolbox > button:not(.rail-line) { @@ -185,6 +186,10 @@ body { font-family: Helvetica, Arial, sans-serif; } +.hidden { + display: none; +} + #title, #autosave-indicator { color: black; font-size: 2rem; @@ -248,7 +253,7 @@ div.mmm-help-setting { left: 0; } -canvas#grid-canvas, canvas#hover-canvas { +canvas#grid-canvas, canvas#hover-canvas, canvas#ruler-canvas { z-index: 100; width: 100%; height: auto; @@ -304,8 +309,31 @@ canvas#grid-canvas, canvas#hover-canvas { border: 0.5rem solid transparent; } +#controls.collapsed #line-color-options { + overflow-x: clip; + width: 100%; +} + +#controls.collapsed #line-color-options fieldset { + border: 0; + padding: 0; +} + #controls.collapsed button.rail-line { - width: 6rem; + overflow-wrap: break-word; + width: 5.333rem; +} + +#controls.collapsed .hide-when-collapsed { + display: none; +} + +#controls:not(.collapsed) #ruler-icon-xy-container { + margin-top: 0.5rem; +} + +#controls.collapsed #ruler-icon-xy-container svg { + display: none; } #tool-line-options button.rail-line:hover { @@ -499,4 +527,5 @@ select { #shareable-map-link { word-break: break-word; -} \ No newline at end of file +} + diff --git a/metro_map_saver/map_saver/static/js/metromapmaker.js b/metro_map_saver/map_saver/static/js/metromapmaker.js index bc98b491..d3f9b7a0 100644 --- a/metro_map_saver/map_saver/static/js/metromapmaker.js +++ b/metro_map_saver/map_saver/static/js/metromapmaker.js @@ -14,27 +14,43 @@ var clickY = false; var hoverX = false; var hoverY = false; var temporaryStation = {}; +var temporaryLabel = {}; var pngUrl = false; var mapHistory = []; // A list of the last several map objects -var MAX_UNDO_HISTORY = 100; +var mapRedoHistory = []; +var MAX_UNDO_HISTORY = 100; // Will reuse this for REDO as well var currentlyClickingAndDragging = false; var mapLineWidth = 1 +var mapLineStyle = 'solid' +// mapLineWidth and mapLineStyle handle the defaults; active are the actively-chosen options +var activeLineWidth = mapLineWidth +var activeLineStyle = mapLineStyle +var activeLineWidthStyle = mapLineWidth + '-' + mapLineStyle var mapStationStyle = 'wmata' var menuIsCollapsed = false var mapSize = undefined // Not the same as gridRows/gridCols, which is the potential size; this gives the current maximum in either axis +var gridStep = 5 +var rulerOn = false +var rulerOrigin = [] var MMMDEBUG = false +var MMMDEBUG_UNDO = false if (typeof mapDataVersion === 'undefined') { var mapDataVersion = undefined } function compatibilityModeIndicator() { // Visual cue to indicate that this is in compatibility mode + // It may be most helpful if the versions progress in ROYGBV order, with black being up to date if (mapDataVersion == 1) { // At a glance, this helps me to see whether I'm on v1 or v2 - $('.M:not(.mobile)').css({"background-color": "#bd1038"}) + $('.M:not(.mobile)').css({"background-color": "#bd1038"}) // red $('#title').css({"color": "#bd1038"}) $('#tool-move-v1-warning').attr('style', '') // Remove the display: none + } else if (mapDataVersion == 2) { + $('.M:not(.mobile)').css({"background-color": "#df8600"}) // orange + $('#title').css({"color": "#df8600"}) + $('#tool-move-v1-warning').attr('style', 'display: none') } else { $('.M:not(.mobile)').css({"background-color": "#000"}) $('#title').css({"color": "#000"}) @@ -44,7 +60,9 @@ function compatibilityModeIndicator() { compatibilityModeIndicator() const numberKeys = ['Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9','Digit0', 'Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9','Digit0', 'Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9','Digit0'] // 1-30; is set up this way to have same functionality on all keyboards -const ALLOWED_ORIENTATIONS = [0, 45, -45, 90, -90, 135, -135, 180]; +const ALLOWED_LINE_WIDTHS = [100, 75.0, 50.0, 25.0, 12.5] +const ALLOWED_LINE_STYLES = ['solid', 'dashed', 'dense_thin', 'dense_thick', 'dotted_dense', 'dotted'] +const ALLOWED_ORIENTATIONS = [0, 45, -45, 90, -90, 135, -135, 180, 1, -1]; const ALLOWED_STYLES = ['wmata', 'rect', 'rect-round', 'circles-lg', 'circles-md', 'circles-sm', 'circles-thin'] const ALLOWED_SIZES = [80, 120, 160, 200, 240, 360] const MAX_MAP_SIZE = ALLOWED_SIZES[ALLOWED_SIZES.length-1] @@ -62,8 +80,30 @@ function resizeGrid(size) { gridRows = size; gridCols = size; - if (mapDataVersion == 2) { - // If the largest grid size changes, I'll need to change it here too + if (mapDataVersion == 3) { + for (var color in activeMap["points_by_color"]) { + for (var lineWidthStyle in activeMap["points_by_color"][color]) { + for (var x=size;x= size && activeMap["points_by_color"][color][lineWidthStyle][x] && activeMap["points_by_color"][color][lineWidthStyle][x][y]) { + delete activeMap["points_by_color"][color][lineWidthStyle][x][y] + } + if (y >= size && activeMap["stations"] && activeMap["stations"][x] && activeMap["stations"][x][y]) { + delete activeMap["stations"][x][y] + } + } // y + } // x + } // lineWidthStyle + } // color + } else if (mapDataVersion == 2) { for (var color in activeMap["points_by_color"]) { for (var x=size;x= 3)), activeToolOption, true) $('#tool-line-icon-pencil').hide() $('#tool-line-icon-paint-bucket').show() } else { @@ -282,18 +376,48 @@ function bindRailLineEvents() { } // bindRailLineEvents() function makeLine(x, y, deferSave) { - // I need to clear the redrawArea first - // BEFORE actually placing the line - // The first call to drawArea() will erase the redrawSection - // The second call actually draws the points - drawArea(x, y, activeMap, true); + if (mapDataVersion == 1) { drawArea(x, y, activeMap, true) } + else if (mapDataVersion >= 2) { + var previousColor = getActiveLine(x, y, activeMap) + } var color = rgb2hex(activeToolOption).slice(1, 7); metroMap = updateMapObject(x, y, "line", color); if (!deferSave) { autoSave(metroMap); } - drawArea(x, y, metroMap); -} + if (mapDataVersion >= 2) { + if (previousColor) { + // If there's nothing here previously, we don't need to clear/redraw + redrawCanvasForColor(previousColor) + } + redrawCanvasForColor(color) + } else if (mapDataVersion == 1) { + drawArea(x, y, activeMap) + } +} // makeLine(x, y, deferSave) + +function redrawCanvasForColor(color) { + var t0 = performance.now() + // Clear the main canvas + drawCanvas(false, false, true) + + // Only redraw the current color; the others we can use as-is + drawColor(color) + + // Draw all of the colors onto the newly-cleared canvas + var canvas = document.getElementById('metro-map-canvas'); + var ctx = canvas.getContext('2d', {alpha: true}); + for (var color in activeMap["points_by_color"]) { + var colorCanvas = createColorCanvasIfNeeded(color) + ctx.drawImage(colorCanvas, 0, 0); // Layer the stations on top of the canvas + } + + // Redraw the stations canvas too, in case any stations were deleted or edited nearby + drawCanvas(activeMap, true) + + var t1 = performance.now() + if (MMMDEBUG) { console.log('redrawCanvasForColor finished in ' + (t1 - t0) + 'ms') } +} // redrawCanvasForColor function makeStation(x, y) { // Use a temporary station and don't write to activeMap unless it actually has data @@ -371,6 +495,66 @@ function makeStation(x, y) { $('#station-name').focus(); // Set focus to the station name box to save you a click each time } // makeStation(x, y) +function makeLabel(x, y) { + // Modeled after makeStation, + // use a temporary label and don't write to activeMap unless it actually has data + temporaryLabel = {} + + // Unlike stations, labels can be placed anywhere -- even if not on a line + $('#tool-label-options').show() + + $('#label-coordinates-x').val(x); + $('#label-coordinates-y').val(y); + + var existingLabel = getLabel(x, y, activeMap) + if (!existingLabel) { + // Create a new label + temporaryLabel = { + "text": "", + "shape": "", + "text-color": "", + "bg-color": "" + } + + // Leave everything else the same as the most-recently used label, but clear the text. + $('#label-text').val('') + + // Set shape to be the last-used shape; + // everything else is too likely to change too often to be able to set defaults + var lastLabelShape = window.localStorage.getItem('lastLabelShape') + if (lastLabelShape) { + document.getElementById('label-shape').value = lastLabelShape; + $('#label-shape').trigger('change') + } + } else { + // Already has a label, so populate the inputs with its current values so it can be edited + + if (existingLabel["text"]) { + $('#label-text').val(existingLabel["text"].replaceAll('_', ' ')) + } + + if (existingLabel["shape"]) { + $('#label-shape').val(existingLabel["shape"]) + } + + if (existingLabel["text-color"]) { + $('#label-text-color').val(existingLabel["text-color"]) + } + + if (existingLabel["bg-color"]) { + $('#label-bg-color').val(existingLabel["bg-color"]) + } else { + $('#label-bg-color').hide() + $('#label-bg-color-transparent').prop('checked', true) + } + } // else (label exists) + + drawCanvas(activeMap, true) + drawLabelIndicator(x, y) + + $('#label-text').focus(); // Set focus to the label box to save you a click each time +} // makeLabel(x, y) + function bindGridSquareEvents(event) { $('#station-coordinates-x').val(''); $('#station-coordinates-y').val(''); @@ -412,44 +596,77 @@ function bindGridSquareEvents(event) { if ($('#tool-flood-fill').prop('checked')){ var initialColor = getActiveLine(x, y, activeMap) var replacementColor = rgb2hex(activeToolOption).slice(1, 7); - floodFill(x, y, initialColor, replacementColor) + floodFill(x, y, getActiveLine(x, y, activeMap, (mapDataVersion >= 3)), replacementColor) autoSave(activeMap) - drawCanvas(activeMap) + if (mapDataVersion >= 2) { + drawColor(initialColor) + redrawCanvasForColor(replacementColor) + } else if (mapDataVersion == 1) { + drawCanvas(activeMap) + } } else makeLine(x, y) } else if (activeTool == 'eraser') { // I need to check for the old line and station // BEFORE actually doing the erase operations var erasedLine = getActiveLine(x, y, activeMap); - if (getStation(x, y, activeMap)) { + if (!erasedLine) { + // Erasing nothing, there's nothing to do here + return + } + + if (getStation(x, y, activeMap) || getLabel(x, y, activeMap)) { + // XXX: Labels share the stations canvas; + // if I ever change that I'll want to have a separate + // check for redrawLabels var redrawStations = true; } else { var redrawStations = false; } + // NOT YET IMPLEMENTED: Check if there is a label here, and if so, redraw Labels after erasing + if (getLabel(x, y, activeMap)) { + var redrawLabels = true + } else { + var redrawLabels = false + } if (erasedLine && $('#tool-flood-fill').prop('checked')) { - floodFill(x, y, erasedLine, '') + floodFill(x, y, getActiveLine(x, y, activeMap, (mapDataVersion >= 3)), '') autoSave(activeMap) - drawCanvas(activeMap) + if (mapDataVersion >= 2) { + redrawCanvasForColor(erasedLine) + } else if (mapDataVersion == 1) { + drawCanvas(activeMap) + } } else { metroMap = updateMapObject(x, y); autoSave(metroMap); - drawArea(x, y, metroMap, erasedLine, redrawStations); + if (mapDataVersion >= 2) { + redrawCanvasForColor(erasedLine) + } else if (mapDataVersion == 1) { + drawArea(x, y, metroMap, erasedLine, redrawStations); + } } } else if (activeTool == 'station') { makeStation(x, y) + } else if (activeTool == 'label') { + makeLabel(x, y) } } // bindGridSquareEvents() function bindGridSquareMouseover(event) { if (MMMDEBUG) { - // $('#title').text([event.pageX, event.pageY, getCanvasXY(event.pageX, event.pageY)]) // useful when debugging - $('#title').text(['XY: ' + getCanvasXY(event.pageX, event.pageY)]) // useful when debugging + // $('#title').text([event.pageX, event.pageY, getCanvasXY(event.pageX, event.pageY)]) + // $('#title').text(['XY: ' + getCanvasXY(event.pageX, event.pageY)]) } + $('#ruler-xy').text(getCanvasXY(event.pageX, event.pageY)) xy = getCanvasXY(event.pageX, event.pageY) hoverX = xy[0] hoverY = xy[1] if (!mouseIsDown && !$('#tool-flood-fill').prop('checked')) { drawHoverIndicator(event.pageX, event.pageY) + if (rulerOn && rulerOrigin.length > 0 && (activeTool == 'look' || activeTool == 'line' || activeTool == 'eraser')) { + drawRuler(hoverX, hoverY) + } } else if (!mouseIsDown && (activeToolOption || activeTool == 'eraser') && $('#tool-flood-fill').prop('checked')) { if (activeTool == 'line' && activeToolOption) { indicatorColor = activeToolOption @@ -459,13 +676,16 @@ function bindGridSquareMouseover(event) { } else { indicatorColor = '#ffffff' } - floodFill(hoverX, hoverY, getActiveLine(hoverX, hoverY, activeMap), indicatorColor, true) + floodFill(hoverX, hoverY, getActiveLine(hoverX, hoverY, activeMap, (mapDataVersion >= 3)), indicatorColor, true) } if (mouseIsDown && (activeTool == 'line' || activeTool == 'eraser')) { dragX = event.pageX dragY = event.pageY $('#canvas-container').click() } + if (mouseIsDown && (rulerOn && rulerOrigin.length > 0 && (activeTool == 'look' || activeTool == 'line' || activeTool == 'eraser'))) { + drawRuler(hoverX, hoverY) + } } // bindGridSquareMouseover() function bindGridSquareMouseup(event) { @@ -483,6 +703,14 @@ function bindGridSquareMouseup(event) { // Immediately clear the straight line assist indicator upon mouseup drawHoverIndicator(event.pageX, event.pageY) + + if (rulerOn && rulerOrigin.length > 0 && (activeTool == 'line' || activeTool == 'eraser')) { + // Reset the rulerOrigin and clear the canvas + rulerOrigin = [] + var canvas = document.getElementById('ruler-canvas') + var ctx = canvas.getContext('2d') + ctx.clearRect(0, 0, canvas.width, canvas.height) + } } function bindGridSquareMousedown(event) { @@ -511,6 +739,10 @@ function bindGridSquareMousedown(event) { mouseIsDown = true currentlyClickingAndDragging = false } + + if (rulerOn) { + drawRuler(clickX, clickY, true) + } } // function bindGridSquareMousedown() function drawHoverIndicator(x, y, fillColor, opacity) { @@ -561,9 +793,40 @@ function drawGrid() { var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.globalAlpha = 0.5 - ctx.strokeStyle = '#80CEFF' gridPixelMultiplier = canvas.width / gridCols; for (var x=0; x 0) { // Ignore the first so we don't get two zeroes next to each other looking silly + var gridX, gridY; + // This looks fiddly, hardcoded, and complex. (Maybe so!) + // But all it's for is to visually center the numbers so they align with the darker line + if (gridCols <= 240) { + gridY = gridPixelMultiplier / 2 + if (x < 10) { + gridX = (x * gridPixelMultiplier) + (gridPixelMultiplier / 4) + 2 + } else if (x < 100) { + gridX = (x * gridPixelMultiplier) + (gridPixelMultiplier / 4) + } else if (x >= 100) { + gridX = (x * gridPixelMultiplier) + (gridPixelMultiplier / 4) - 4 + } + } else if (gridCols > 240) { + gridY = gridPixelMultiplier / 1.25 + if (x < 10) { + gridX = (x * gridPixelMultiplier) + (gridPixelMultiplier / 4) + } else if (x < 100) { + gridX = (x * gridPixelMultiplier) + (gridPixelMultiplier / 4) - 3 + } else if (x < 1000) { + gridX = (x * gridPixelMultiplier) + (gridPixelMultiplier / 4) - 6 + } + } + ctx.fillText(x, gridX, gridY); + // And at the bottom + ctx.fillText(x, gridX, (canvas.height - (gridPixelMultiplier / 4))); + } + } else { + ctx.strokeStyle = '#80CEFF' + } ctx.beginPath() ctx.moveTo((x * gridPixelMultiplier) + (gridPixelMultiplier / 2), 0); ctx.lineTo((x * gridPixelMultiplier) + (gridPixelMultiplier / 2), canvas.height); @@ -571,12 +834,28 @@ function drawGrid() { ctx.closePath() } for (var y=0; y 0) { // Don't show the first 0 on the right side + ctx.fillText(y, (canvas.width - gridMarkOffset.width - (gridPixelMultiplier / 4)), (y * gridPixelMultiplier) + (gridPixelMultiplier / 2) + 3); + } + } else { + ctx.strokeStyle = '#80CEFF' + } ctx.beginPath() ctx.moveTo(0, (y * gridPixelMultiplier) + (gridPixelMultiplier / 2)); ctx.lineTo(canvas.width, (y * gridPixelMultiplier) + (gridPixelMultiplier / 2)); ctx.stroke() ctx.closePath() } + if (!gridStep) { + $('#grid-step').text('off') + } else { + $('#grid-step').text(gridStep) + } } // drawGrid() function getRedrawSection(x, y, metroMap, redrawRadius) { @@ -606,16 +885,15 @@ function drawArea(x, y, metroMap, erasedLine, redrawStations) { var canvas = document.getElementById('metro-map-canvas'); var ctx = canvas.getContext('2d', {alpha: false}); gridPixelMultiplier = canvas.width / gridCols; - var fontSize = 20; - if (gridPixelMultiplier > 20) - fontSize = gridPixelMultiplier + fontSize = gridPixelMultiplier var redrawRadius = 1; x = parseInt(x); y = parseInt(y); - ctx.lineWidth = gridPixelMultiplier * mapLineWidth; + // ctx.lineWidth = gridPixelMultiplier * mapLineWidth; + ctx.lineWidth = gridPixelMultiplier * activeLineWidth; ctx.lineCap = 'round'; if (activeTool == 'eraser') { @@ -654,6 +932,7 @@ function drawArea(x, y, metroMap, erasedLine, redrawStations) { x = parseInt(x); y = parseInt(y); drawStation(ctxStations, x, y, metroMap, true) + drawLabel(ctxStations, x, y, metroMap) } // for y } // for x } else if (redrawStations) { @@ -663,7 +942,7 @@ function drawArea(x, y, metroMap, erasedLine, redrawStations) { ctxStations.clearRect(0, 0, canvasStations.width, canvasStations.height); ctxStations.font = '700 ' + fontSize + 'px sans-serif'; - if (mapDataVersion == 2) { + if (mapDataVersion == 2 || mapDataVersion == 3) { for (var x in metroMap['stations']) { for (var y in metroMap['stations'][x]) { x = parseInt(x); @@ -671,7 +950,17 @@ function drawArea(x, y, metroMap, erasedLine, redrawStations) { drawStation(ctxStations, x, y, metroMap); } // for y } // for x - } + // XXX: As long as I'm re-using the metro-map-stations-canvas + // for the labels, I'll need to re-draw them here. + // (That might be reason enough to not re-use the canvas!) + for (var x in metroMap['labels']) { + for (var y in metroMap['labels'][x]) { + x = parseInt(x) + y = parseInt(y) + drawLabel(ctxStations, x, y, metroMap) + } // for y + } // for x + } // mapDataVersion (2, 3) else if (mapDataVersion == 1) { for (var x in metroMap) { for (var y in metroMap[x]) { @@ -687,9 +976,97 @@ function drawArea(x, y, metroMap, erasedLine, redrawStations) { } // if redrawStations } // drawArea(x, y, metroMap, redrawStations) -function findLines(color) { +function drawColor(color) { + // Draws only a single color + if (!color) { + // When flood filling on an empty area, the initial color is undefined, + // so end here -- there's nothing to do. + return + } + var colorCanvas = createColorCanvasIfNeeded(color) + var ctx = colorCanvas.getContext('2d', {alpha: true}) + ctx.clearRect(0, 0, colorCanvas.width, colorCanvas.height); + if (mapDataVersion == 3) { + for (var lineWidthStyle in activeMap['points_by_color'][color]) { + ctx.strokeStyle = '#' + color + var thisLineWidth = lineWidthStyle.split('-')[0] * gridPixelMultiplier + var thisLineStyle = lineWidthStyle.split('-')[1] + var linesAndSingletons = findLines(color, lineWidthStyle) + var lines = linesAndSingletons["lines"] + var singletons = linesAndSingletons["singletons"] + for (var line of lines) { + ctx.beginPath() + ctx.lineWidth = thisLineWidth + setLineStyle(thisLineStyle, ctx) + moveLineStroke(ctx, line[0], line[1], line[2], line[3]) + ctx.stroke() + ctx.closePath() + } + for (var s of singletons) { + var xy = s.split(',') + var x = xy[0] + var y = xy[1] + ctx.strokeStyle = '#' + color + drawPoint(ctx, x, y, activeMap, false, color, thisLineWidth, thisLineStyle) + } + } // lineWidthStyle + } else if (mapDataVersion == 2) { + ctx.strokeStyle = '#' + color + if (activeMap && activeMap['global'] && activeMap['global']['style']) { + ctx.lineWidth = (activeMap['global']['style']['mapLineWidth'] || 1) * gridPixelMultiplier + } else { + ctx.lineWidth = mapLineWidth * gridPixelMultiplier + } + ctx.lineCap = 'round'; + var linesAndSingletons = findLines(color) + var lines = linesAndSingletons["lines"] + var singletons = linesAndSingletons["singletons"] + for (var line of lines) { + ctx.beginPath() + moveLineStroke(ctx, line[0], line[1], line[2], line[3]) + ctx.stroke() + ctx.closePath() + } + for (var s of singletons) { + var xy = s.split(',') + var x = xy[0] + var y = xy[1] + ctx.strokeStyle = '#' + color + drawPoint(ctx, x, y, activeMap, false, color) + } // singletons + } // mapDataVersion +} // drawColor(ctx, color) + +function createColorCanvasIfNeeded(color, resize) { + var colorCanvas = document.getElementById('metro-map-color-canvas-' + color) + if (!colorCanvas) { + var mmCanvas = document.getElementById('metro-map-canvas') + var colorCanvasContainer = document.getElementById('color-canvas-container') + var colorCanvas = document.createElement("canvas") + colorCanvas.id = "metro-map-color-canvas-" + color + colorCanvas.classList = 'hidden' + colorCanvas.width = mmCanvas.width + colorCanvas.height = mmCanvas.height + colorCanvasContainer.appendChild(colorCanvas) + } + if (resize) { + var mmCanvas = document.getElementById('metro-map-canvas') + colorCanvas.width = mmCanvas.width + colorCanvas.height = mmCanvas.height + } + return colorCanvas +} // createColorCanvasIfNeeded(color) + +function findLines(color, lineWidthStyle) { // JS implementation of mapdata_optimizer.find_lines - if (!activeMap || !activeMap['points_by_color'] || !activeMap['points_by_color'][color] || !activeMap['points_by_color'][color]['xys']) { + + if (mapDataVersion == 2 && !lineWidthStyle) { + // 'xys' isn't the width or style, but this avoids repeating essentially the same code + // and keeps the functions compatible between mapDataVersion 2 and 3 + lineWidthStyle = 'xys' + } + + if ((mapDataVersion == 2 || mapDataVersion == 3) && (!activeMap || !activeMap['points_by_color'] || !activeMap['points_by_color'][color] || !activeMap['points_by_color'][color][lineWidthStyle])) { return } @@ -706,13 +1083,13 @@ function findLines(color) { var notSingletons = new Set() for (var direction of directions) { - for (var x in activeMap['points_by_color'][color]['xys']) { - for (var y in activeMap['points_by_color'][color]['xys'][x]) { + for (var x in activeMap['points_by_color'][color][lineWidthStyle]) { + for (var y in activeMap['points_by_color'][color][lineWidthStyle][x]) { var point = x + ',' + y if (skipPoints[direction].has(point)) { continue } - var endpoint = findEndpointOfLine(x, y, activeMap['points_by_color'][color]['xys'], direction) + var endpoint = findEndpointOfLine(x, y, activeMap['points_by_color'][color][lineWidthStyle], direction) if (endpoint) { lines.push([parseInt(x), parseInt(y), endpoint['x1'], endpoint['y1']]) notSingletons.add(point) @@ -827,26 +1204,12 @@ function drawCanvas(metroMap, stationsOnly, clearOnly) { ctx.lineWidth = mapLineWidth * gridPixelMultiplier ctx.lineCap = 'round'; - if (mapDataVersion == 2) { + if (mapDataVersion >= 2) { for (var color in metroMap['points_by_color']) { - ctx.strokeStyle = '#' + color - var linesAndSingletons = findLines(color) - var lines = linesAndSingletons["lines"] - var singletons = linesAndSingletons["singletons"] - for (var line of lines) { - ctx.beginPath() - moveLineStroke(ctx, line[0], line[1], line[2], line[3]) - ctx.stroke() - ctx.closePath() - } - for (var s of singletons) { - var xy = s.split(',') - var x = xy[0] - var y = xy[1] - ctx.strokeStyle = '#' + color - drawPoint(ctx, x, y, metroMap, false, color) - } - } + drawColor(color) + var colorCanvas = document.getElementById('metro-map-color-canvas-' + color) + ctx.drawImage(colorCanvas, 0, 0); // Layer the stations on top of the canvas + } // color } else if (mapDataVersion == 1) { for (var x in metroMap) { for (var y in metroMap[x]) { @@ -883,14 +1246,24 @@ function drawCanvas(metroMap, stationsOnly, clearOnly) { ctx.clearRect(0, 0, canvas.width, canvas.height); - if (mapDataVersion == 2) { + if (mapDataVersion == 3 || mapDataVersion == 2) { for (var x in metroMap['stations']) { for (var y in metroMap['stations'][x]) { x = parseInt(x); y = parseInt(y); drawStation(ctx, x, y, metroMap) } - } + } // stations + for (var x in metroMap['labels']) { + for (var y in metroMap['labels'][x]) { + x = parseInt(x) + y = parseInt(y) + // Currently, labels are drawn on the stations canvas. + // There's no reason that they couldn't have their own canvas, + // but it seems fine to re-use the stations canvas. + drawLabel(ctx, x, y, metroMap) + } + } // labels } else if (mapDataVersion == 1) { for (var x in metroMap){ for (var y in metroMap[x]) { @@ -905,6 +1278,7 @@ function drawCanvas(metroMap, stationsOnly, clearOnly) { } // mapType/dataVersion check // Add a map credit to help promote the site + ctx.textAlign = 'start' ctx.font = '700 ' + fontSize + 'px sans-serif'; ctx.fillStyle = '#000000'; var mapCredit = 'Created with MetroMapMaker.com'; @@ -922,12 +1296,13 @@ function drawCanvas(metroMap, stationsOnly, clearOnly) { } } t1 = performance.now(); - if (MMMDEBUG) { console.log('drawCanvas finished in ' + (t1 - t0) + 'ms') } -} // drawCanvas(metroMap) + if (MMMDEBUG) { console.log('drawCanvas(map, ' + stationsOnly + ', ' + clearOnly + ') finished in ' + (t1 - t0) + 'ms') } +} // drawCanvas(metroMap, stationsOnly, clearOnly) -function drawPoint(ctx, x, y, metroMap, erasedLine, color, lineWidth) { +function drawPoint(ctx, x, y, metroMap, erasedLine, color, lineWidth, lineStyle) { // Draw a single point at position x, y + x = parseInt(x) y = parseInt(y) var color = color || getActiveLine(x, y, metroMap) @@ -950,11 +1325,21 @@ function drawPoint(ctx, x, y, metroMap, erasedLine, color, lineWidth) { ctx.strokeStyle = '#ffffff'; color = erasedLine; } + else { + ctx.lineWidth = (gridPixelMultiplier * (lineWidth || activeLineWidth)) + setLineStyle(lineStyle || activeLineStyle, ctx) + } singleton = true; + if (lineWidth && lineStyle) { + var thisLineWidthStyle = lineWidth + '-' + lineStyle + } else { + var thisLineWidthStyle = activeLineWidthStyle + } + // Diagonals - if (coordinateInColor(x + 1, y + 1, metroMap, color)) { + if (coordinateInColor(x + 1, y + 1, metroMap, color, thisLineWidthStyle)) { // Direction: SE moveLineStroke(ctx, x, y, x+1, y+1) // If this southeast line is adjacent to a different color on its east, @@ -963,7 +1348,7 @@ function drawPoint(ctx, x, y, metroMap, erasedLine, color, lineWidth) { redrawOverlappingPoints[x] = {} } redrawOverlappingPoints[x][y] = true - } if (coordinateInColor(x - 1, y - 1, metroMap, color)) { + } if (coordinateInColor(x - 1, y - 1, metroMap, color, thisLineWidthStyle)) { // Direction: NW // Since the drawing goes left -> right, top -> bottom, // I don't need to draw NW if I've drawn SE @@ -972,25 +1357,25 @@ function drawPoint(ctx, x, y, metroMap, erasedLine, color, lineWidth) { // But now that I'm using drawPoint() inside of drawArea(), // I can't rely on this shortcut anymore. (only a problem with mapDataVersion 1) moveLineStroke(ctx, x, y, x-1, y-1) - } if (coordinateInColor(x + 1, y - 1, metroMap, color)) { + } if (coordinateInColor(x + 1, y - 1, metroMap, color, thisLineWidthStyle)) { // Direction: NE moveLineStroke(ctx, x, y, x+1, y-1) - } if (coordinateInColor(x - 1, y + 1, metroMap, color)) { + } if (coordinateInColor(x - 1, y + 1, metroMap, color, thisLineWidthStyle)) { // Direction: SW moveLineStroke(ctx, x, y, x-1, y+1) } // Cardinals - if (coordinateInColor(x + 1, y, metroMap, color)) { + if (coordinateInColor(x + 1, y, metroMap, color, thisLineWidthStyle)) { // Direction: E moveLineStroke(ctx, x, y, x+1, y) - } if (coordinateInColor(x - 1, y, metroMap, color)) { + } if (coordinateInColor(x - 1, y, metroMap, color, thisLineWidthStyle)) { // Direction: W moveLineStroke(ctx, x, y, x-1, y) - } if (coordinateInColor(x, y + 1, metroMap, color)) { + } if (coordinateInColor(x, y + 1, metroMap, color, thisLineWidthStyle)) { // Direction: S moveLineStroke(ctx, x, y, x, y+1) - } if (coordinateInColor(x, y - 1, metroMap, color)) { + } if (coordinateInColor(x, y - 1, metroMap, color, thisLineWidthStyle)) { // Direction: N moveLineStroke(ctx, x, y, x, y-1) } @@ -1059,6 +1444,7 @@ function drawStation(ctx, x, y, metroMap, skipText) { function drawStationName(ctx, x, y, metroMap, isTransferStation, drawAsConnected) { // Write the station name + ctx.textAlign = 'start' ctx.fillStyle = '#000000'; ctx.save(); var station = getStation(x, y, metroMap) @@ -1106,6 +1492,20 @@ function drawStationName(ctx, x, y, metroMap, isTransferStation, drawAsConnected } else { ctx.fillText(stationName, (x * gridPixelMultiplier) - (gridPixelMultiplier) - textSize, (y * gridPixelMultiplier) + gridPixelMultiplier / 4); } + } else if (orientation == 1) { // Above + ctx.textAlign = 'center' + if (isTransferStation) { + ctx.fillText(stationName, (x * gridPixelMultiplier), ((y - 1.5) * gridPixelMultiplier)) + } else { + ctx.fillText(stationName, (x * gridPixelMultiplier), ((y - 1) * gridPixelMultiplier)) + } + } else if (orientation == -1) { // Below + ctx.textAlign = 'center' + if (isTransferStation) { + ctx.fillText(stationName, (x * gridPixelMultiplier), ((y + 2.25) * gridPixelMultiplier)) + } else { + ctx.fillText(stationName, (x * gridPixelMultiplier), ((y + 1.75) * gridPixelMultiplier)) + } } else { if (isTransferStation) { ctx.fillText(stationName, (x * gridPixelMultiplier) + (gridPixelMultiplier * 1.5), (y * gridPixelMultiplier) + gridPixelMultiplier / 4); @@ -1152,7 +1552,17 @@ function drawCircleStation(ctx, x, y, metroMap, isTransferStation, stationCircle } function drawStyledStation_rectangles(ctx, x, y, metroMap, isTransferStation, strokeColor, fillColor, radius, isIndicator) { - var lineColor = '#' + getActiveLine(x, y, metroMap) + var lineColorWidthStyle = getActiveLine(x, y, metroMap, true) + if (mapDataVersion == 3) { + var lineColor = '#' + lineColorWidthStyle[0] + var lineWidth = lineColorWidthStyle[1].split('-')[0] + } else if (mapDataVersion == 2) { + var lineColor = '#' + lineColorWidthStyle + var lineWidth = mapLineWidth + } else if (mapDataVersion == 1) { + var lineColor = '#' + lineColorWidthStyle + var lineWidth = 1 + } var lineDirection = getLineDirection(x, y, metroMap)["direction"] var rectArgs = [] @@ -1163,7 +1573,7 @@ function drawStyledStation_rectangles(ctx, x, y, metroMap, isTransferStation, st var width = gridPixelMultiplier var height = gridPixelMultiplier - if (mapLineWidth >= 0.5 && lineDirection != 'singleton') { + if (lineWidth >= 0.5 && lineDirection != 'singleton') { // If the lines are thick and this POINT isn't a singleton, // the rectangles should be black/white so they can be legible ctx.strokeStyle = '#000000' @@ -1218,7 +1628,7 @@ function drawStyledStation_rectangles(ctx, x, y, metroMap, isTransferStation, st if (lineDirection == 'singleton') { // Keep original width ctx.strokeStyle = '#000000' - if (mapLineWidth < 0.5 && (mapStationStyle == 'rect-round' || thisStation && thisStation['style'] == 'rect-round')) { + if (lineWidth < 0.5 && (mapStationStyle == 'rect-round' || thisStation && thisStation['style'] == 'rect-round')) { ctx.fillStyle = lineColor } } else if (!isTransferStation) { @@ -1411,6 +1821,105 @@ function drawIndicator(x, y) { } } // drawIndicator(x, y) +function drawLabel(ctx, x, y, metroMap, indicatorColor) { + var label = getLabel(x, y, metroMap) + + if (!label && !indicatorColor) { + // If there's no label here (and it's not an indicator), end + return + } else if (indicatorColor && !label) { + // This is an indicator only, + // let's see if a placeholder looks nice? + label = { + 'text': 'Label', + 'shape': $('#label-shape').val(), + 'text-color': '#333333' + } + } + + var textWidth = ctx.measureText(label['text']).width + var labelShapeWidth; + var shapeArgs; + + if (textWidth < gridPixelMultiplier) { + labelShapeWidth = getWidthToNearestGrid(gridPixelMultiplier * 1.25) + } else { + labelShapeWidth = getWidthToNearestGrid(parseInt(textWidth * 1.33)) + } + var labelShapeHeight = gridPixelMultiplier + + // NOT YET IMPLEMENTED: + // Consider different font size or weight maybe + // Consider a different horizontal placement for the text + // Consider different circle sizes + + if (label["bg-color"] || indicatorColor) { + if (indicatorColor) { + ctx.fillStyle = indicatorColor + ctx.strokeStyle = indicatorColor + } else { + ctx.fillStyle = label["bg-color"] + ctx.strokeStyle = label["bg-color"] + } + + if (label["shape"] == 'rect') { + shapeArgs = [(x - 0.5) * gridPixelMultiplier, (y - 0.5) * gridPixelMultiplier, labelShapeWidth, labelShapeHeight] + ctx.strokeRect(...shapeArgs) + ctx.fillRect(...shapeArgs) + } else if (label["shape"] == 'rect-round') { + shapeArgs = [(x - 0.5) * gridPixelMultiplier, (y - 0.5) * gridPixelMultiplier, labelShapeWidth, labelShapeHeight] + primitiveRoundRect(ctx, ...shapeArgs, 2) + } else if (label["shape"] == 'circle') { + shapeArgs = [x * gridPixelMultiplier, y * gridPixelMultiplier, gridPixelMultiplier, 0, Math.PI * 2] + ctx.beginPath(); + ctx.arc(...shapeArgs); + ctx.closePath(); + ctx.stroke() + ctx.fill() + } else if (label["shape"] == 'square') { + shapeArgs = [(x - 0.5) * gridPixelMultiplier, (y - 0.5) * gridPixelMultiplier, gridPixelMultiplier, gridPixelMultiplier] + ctx.strokeRect(...shapeArgs) + ctx.fillRect(...shapeArgs) + } + } // label: bg-color; If there's no background color, there's no shape. + + if (label["text"]) { + ctx.font = '700 ' + gridPixelMultiplier + 'px Helvetica,sans-serif'; + ctx.fillStyle = label['text-color'] + + var textX, maxWidth; + textX = (x * gridPixelMultiplier) + if (label["shape"] == 'circle' || label["shape"] == 'square') { + maxWidth = gridPixelMultiplier + ctx.textAlign = 'center' + } else if (label["shape"] == 'oval') { + ctx.textAlign = 'center' + } else { + ctx.textAlign = 'start' + } + ctx.fillText(label["text"], textX, (y * gridPixelMultiplier) + parseInt(gridPixelMultiplier / 3), maxWidth); + } +} // drawLabel(ctx, x, y, metroMap) + +function drawLabelIndicator(x, y) { + // Place a temporary label marker on the canvas + // (which is overwritten by drawCanvas later) + var canvas = document.getElementById('metro-map-stations-canvas') + var ctx = canvas.getContext('2d', {alpha: false}) + var gridPixelMultiplier = canvas.width / gridCols + drawLabel(ctx, x, y, activeMap, '#00ff00') +} + +function getWidthToNearestGrid(width) { + // Returns width to the nearest gridPixelMultiplier, rounded up. + var modulo = width % gridPixelMultiplier + if (modulo > 0) { + return (Math.floor(width / gridPixelMultiplier) * gridPixelMultiplier) + gridPixelMultiplier + } else { + return (Math.floor(width / gridPixelMultiplier) * gridPixelMultiplier) + } +} // getWidthToNearestGrid(width) + function rgb2hex(rgb) { if (/^#[0-9A-F]{6}$/i.test(rgb)) return rgb; @@ -1433,6 +1942,8 @@ function saveMapHistory(metroMap) { // happening AFTER the call to saveMapHistory if (JSON.stringify(metroMap) != window.localStorage.getItem('metroMap')) { mapHistory.push(JSON.stringify(metroMap)) + $('span#undo-buffer-count').text('(' + mapHistory.length + ')') + $('#tool-undo').prop('disabled', false) } debugUndoRedo() @@ -1440,12 +1951,16 @@ function saveMapHistory(metroMap) { function autoSave(metroMap) { // Saves the provided metroMap to localStorage + // This should be called AFTER the event that changes the map, not before. if (typeof metroMap == 'object') { activeMap = metroMap; saveMapHistory(activeMap) metroMap = JSON.stringify(metroMap); } window.localStorage.setItem('metroMap', metroMap); // IMPORTANT: this must happen after saveMapHistory(activeMap) + mapRedoHistory = [] // Clear the redo buffer + $('#tool-redo').prop('disabled', true) + $('span#redo-buffer-count').text('') if (!menuIsCollapsed) { $('#autosave-indicator').text('Saving locally ...'); $('#title').hide() @@ -1456,24 +1971,14 @@ function autoSave(metroMap) { } } // autoSave(metroMap) -function undo() { - // Rewind to an earlier map in the mapHistory - var previousMap = false - if (mapHistory.length > 1) { - mapHistory.pop(); // Remove the most recently added item in the history - previousMap = mapHistory[mapHistory.length-1] - } else if (mapHistory.length == 1) { - previousMap = mapHistory[0] - } - - debugUndoRedo(); +function loadMapFromUndoRedo(previousMap) { if (previousMap) { window.localStorage.setItem('metroMap', previousMap) // Otherwise, undoing and then loading the page before making at least 1 change will result in losing whatever changes were made since the last autoSave // Remove all rail lines, they'll be replaced on loadMapFromObject() $('.rail-line').remove(); - previousMap = JSON.parse(previousMap) + var previousMap = JSON.parse(previousMap) loadMapFromObject(previousMap) - setMapSize(previousMap) + setMapSize(previousMap, true) drawCanvas(previousMap) if (previousMap['global'] && previousMap['global']['style']) { mapLineWidth = previousMap['global']['style']['mapLineWidth'] || mapLineWidth || 1 @@ -1484,11 +1989,66 @@ function undo() { } resetResizeButtons(gridCols) resetRailLineTooltips() + } // if (previousMap) +} // loadMapFromUndoRedo(previousMap) + +function undo() { + // Rewind to an earlier map in the mapHistory + var previousMap = false + if (mapHistory.length > 1) { + var currentMap = mapHistory.pop(); // Remove the most recently added item in the history + previousMap = mapHistory[mapHistory.length-1] + $('span#undo-buffer-count').text('(' + mapHistory.length + ')') + } else if (mapHistory.length == 1) { + previousMap = mapHistory[0] + $('span#undo-buffer-count').text('') + } + + if (mapRedoHistory.length > MAX_UNDO_HISTORY) { + mapRedoHistory.shift() + } + if ((currentMap || previousMap) != mapRedoHistory[mapRedoHistory.length-1]) { + mapRedoHistory.push(currentMap || previousMap) + } else { + return } + $('#tool-redo').prop('disabled', false) + $('span#redo-buffer-count').text('(' + mapRedoHistory.length + ')') + + debugUndoRedo(); + loadMapFromUndoRedo(previousMap) + $('.tooltip').hide() } // undo() +function redo() { + // After undoing, redo will allow you to undo the undo + var previousMap = false + if (mapRedoHistory.length >= 1) { + previousMap = mapRedoHistory.pop() + mapHistory.push(previousMap) + } else { + return + } + + + $('span#undo-buffer-count').text('(' + mapHistory.length + ')') + if (!previousMap) { + $('span#redo-buffer-count').text('') + return + } + + if (mapRedoHistory.length == 0) { + $('span#redo-buffer-count').text('') + } else { + $('span#redo-buffer-count').text('(' + mapRedoHistory.length + ')') + } + + loadMapFromUndoRedo(previousMap) + $('.tooltip').hide() +} // redo() + function debugUndoRedo() { - if (MMMDEBUG) { + if (MMMDEBUG && MMMDEBUG_UNDO) { $('#announcement').html('') for (var i=0;i= mapDataVersion) { + upgradeMapDataVersion(desiredMapDataVersion) + } else { + try { + upgradeMapDataVersion() + } catch (e) { + console.warn('Error when trying to upgradeMapDataVersion(): ' + e) + } + } + compatibilityModeIndicator() mapSize = setMapSize(activeMap, mapDataVersion > 1) loadMapFromObject(activeMap) @@ -1604,7 +2181,29 @@ function getMapSize(metroMapObject) { metroMapObject = JSON.parse(metroMapObject); } - if (mapDataVersion == 2) { + if (mapDataVersion == 3) { + for (var color in metroMapObject['points_by_color']) { + for (var lineWidthStyle in metroMapObject['points_by_color'][color]) { + thisColorHighestValue = Math.max(...Object.keys(metroMapObject['points_by_color'][color][lineWidthStyle]).map(Number).filter(Number.isInteger).filter(function (key) { + return Object.keys(metroMapObject['points_by_color'][color][lineWidthStyle][key]).length > 0 + })) + for (var x in metroMapObject['points_by_color'][color][lineWidthStyle]) { + if (thisColorHighestValue >= ALLOWED_SIZES[ALLOWED_SIZES.length-2]) { + highestValue = thisColorHighestValue + break + } + // getMapSize(activeMap) + y = Math.max(...Object.keys(metroMapObject['points_by_color'][color][lineWidthStyle][x]).map(Number).filter(Number.isInteger)) + if (y > thisColorHighestValue) { + thisColorHighestValue = y; + } + } // y + if (thisColorHighestValue > highestValue) { + highestValue = thisColorHighestValue + } + } // lineWidthStyle + } // color + } else if (mapDataVersion == 2) { for (var color in metroMapObject['points_by_color']) { thisColorHighestValue = Math.max(...Object.keys(metroMapObject['points_by_color'][color]['xys']).map(Number).filter(Number.isInteger).filter(function (key) { return Object.keys(metroMapObject['points_by_color'][color]['xys'][key]).length > 0 @@ -1679,7 +2278,7 @@ function setMapSize(metroMapObject, getFromGlobal) { } // setMapSize(metroMapObject) function setMapStyle(metroMap) { - if (metroMap['global'] && metroMap['global']['style']) { + if (metroMap && metroMap['global'] && metroMap['global']['style']) { mapLineWidth = metroMap['global']['style']['mapLineWidth'] || mapLineWidth mapStationStyle = metroMap['global']['style']['mapStationStyle'] || mapStationStyle } @@ -1734,7 +2333,7 @@ function loadMapFromObject(metroMapObject, update) { } else { keyboardShortcut = '' } - $('#rail-line-new').before(''); + $('#line-color-options fieldset').append(''); numLines++; } } @@ -1766,15 +2365,37 @@ function updateMapObject(x, y, key, data) { var metroMap = JSON.parse(window.localStorage.getItem('metroMap')); } - if (mapDataVersion == 2 && activeTool == 'line') { + if (mapDataVersion == 3 && activeTool == 'line') { // If the map was cleared, let's make sure we can add to it - if (!metroMap['points_by_color'][data] || !metroMap['points_by_color'][data]["xys"]) { - metroMap['points_by_color'][data] = {"xys": {}} + if (!metroMap['points_by_color'][data]) { + metroMap['points_by_color'][data] = {} + } + + if (!metroMap['points_by_color'][data][activeLineWidthStyle]) { + metroMap['points_by_color'][data][activeLineWidthStyle] = {} + } + } else if (mapDataVersion == 2 && activeTool == 'line') { + // If the map was cleared, let's make sure we can add to it + if (!metroMap['points_by_color'][data] || !metroMap['points_by_color'][data]['xys']) { + metroMap['points_by_color'][data] = {'xys': {}} } } if (activeTool == 'eraser') { - if (mapDataVersion == 2) { + if (mapDataVersion == 3) { + if (!data) { data = getActiveLine(x, y, metroMap) } + for (var lineWidthStyle in metroMap['points_by_color'][data]) { + if (metroMap['points_by_color'] && metroMap['points_by_color'][data] && metroMap['points_by_color'][data][lineWidthStyle] && metroMap['points_by_color'][data][lineWidthStyle][x] && metroMap['points_by_color'][data][lineWidthStyle][x][y]) { + delete metroMap['points_by_color'][data][lineWidthStyle][x][y] + } + } + if (metroMap["stations"] && metroMap["stations"][x] && metroMap["stations"][x][y]) { + delete metroMap["stations"][x][y] + } + if (metroMap["labels"] && metroMap["labels"][x] && metroMap["labels"][x][y]) { + delete metroMap["labels"][x][y] + } + } else if (mapDataVersion == 2) { if (!data) { data = getActiveLine(x, y, metroMap) } if (metroMap['points_by_color'] && metroMap['points_by_color'][data] && metroMap['points_by_color'][data]['xys'] && metroMap['points_by_color'][data]['xys'][x] && metroMap['points_by_color'][data]['xys'][x][y]) { delete metroMap['points_by_color'][data]['xys'][x][y] @@ -1782,6 +2403,9 @@ function updateMapObject(x, y, key, data) { if (metroMap["stations"] && metroMap["stations"][x] && metroMap["stations"][x][y]) { delete metroMap["stations"][x][y] } + if (metroMap["labels"] && metroMap["labels"][x] && metroMap["labels"][x][y]) { + delete metroMap["labels"][x][y] + } } else if (mapDataVersion == 1) { if (metroMap[x] && metroMap[x][y]) { delete metroMap[x][y] @@ -1790,7 +2414,7 @@ function updateMapObject(x, y, key, data) { return metroMap; } - // v2 is handled below, in line (with color) and station sections + // v2 & v3 is handled below, in line (with color) and station sections if (mapDataVersion == 1) { if (!metroMap.hasOwnProperty(x)) { metroMap[x] = {}; @@ -1802,7 +2426,24 @@ function updateMapObject(x, y, key, data) { } } - if (mapDataVersion == 2 && activeTool == 'line') { + if (mapDataVersion == 3 && activeTool == 'line') { + // points_by_color, data, activeLineWidthStyle were added above + if (!metroMap['points_by_color'][data][activeLineWidthStyle][x]) { + metroMap['points_by_color'][data][activeLineWidthStyle][x] = {} + } + metroMap['points_by_color'][data][activeLineWidthStyle][x][y] = 1 + // But I also need to make sure no other colors and lineWidthStyles have this x,y coordinate anymore. + for (var color in metroMap['points_by_color']) { + for (var lineWidthStyle in metroMap['points_by_color'][color]) { + if (color == data && lineWidthStyle == activeLineWidthStyle) { + continue + } + if (metroMap['points_by_color'][color][lineWidthStyle] && metroMap['points_by_color'][color][lineWidthStyle][x] && metroMap['points_by_color'][color][lineWidthStyle][x][y]) { + delete metroMap['points_by_color'][color][lineWidthStyle][x][y] + } + } // lineWidthStyle + } // color + } else if (mapDataVersion == 2 && activeTool == 'line') { // points_by_color, data, xys were added above if (!metroMap['points_by_color'][data]['xys'][x]) { metroMap['points_by_color'][data]['xys'][x] = {} @@ -1820,7 +2461,7 @@ function updateMapObject(x, y, key, data) { } else if (mapDataVersion == 1 && activeTool == 'line') { metroMap[x][y]["line"] = data; } else if (activeTool == 'station') { - if (mapDataVersion == 2) { + if (mapDataVersion == 2 || mapDataVersion == 3) { if (!metroMap['stations']) { metroMap['stations'] = {} } @@ -1834,6 +2475,23 @@ function updateMapObject(x, y, key, data) { } else { metroMap[x][y]["station"][key] = data; } + } else if (activeTool == 'label') { + if (mapDataVersion == 2 || mapDataVersion == 3) { + if (!metroMap['labels']) { + metroMap['labels'] = {} + } + if (!metroMap['labels'][x]) { + metroMap['labels'][x] = {} + } + if (!metroMap['labels'][x][y]) { + metroMap['labels'][x][y] = {} + } + if (key) { + metroMap["labels"][x][y][key] = data + } else { + metroMap["labels"][x][y] = data + } + } } return metroMap; @@ -1861,7 +2519,50 @@ function moveMap(direction) { var yOffset = -1; } - if (mapDataVersion == 2) { + if (mapDataVersion == 3) { + var newPointsByColor = {} + var newStations = {} + for (var color in activeMap['points_by_color']) { + newPointsByColor[color] = {} + for (var lineWidthStyle in activeMap['points_by_color'][color]) { + newPointsByColor[color][lineWidthStyle] = {} + for (var x in activeMap['points_by_color'][color][lineWidthStyle]) { + for (var y in activeMap['points_by_color'][color][lineWidthStyle][x]) { + x = parseInt(x); + y = parseInt(y); + if (!Number.isInteger(x) || !Number.isInteger(y)) { + continue; + } + + if (!newPointsByColor[color][lineWidthStyle][x + xOffset]) { + newPointsByColor[color][lineWidthStyle][x + xOffset] = {} + } + + // Prevent going out of bounds + if (x == 0 && direction == 'left') { return } + else if (x == (gridCols - 1) && direction == 'right') { return } + else if (y == 0 && direction == 'up') { return } + else if (y == (gridRows - 1) && direction == 'down') { return } + + // If x,y is within the boundaries + if ((0 <= x && x < gridCols && 0 <= y && y < gridCols)) { + // If the next square is within the boundaries + if (0 <= x + xOffset && x + xOffset < gridCols && 0 <= y + yOffset && y + yOffset < gridCols) { + newPointsByColor[color][lineWidthStyle][x + xOffset][y + yOffset] = activeMap['points_by_color'][color][lineWidthStyle][x][y] + if (activeMap['stations'] && activeMap['stations'][x] && activeMap['stations'][x][y]) { + // v2 drawback is needing to do stations and lines separately + if (!newStations[x + xOffset]) { newStations[x + xOffset] = {} } + newStations[x + xOffset][y + yOffset] = activeMap['stations'][x][y] + } // if stations + } // next within boundaries + } // x,y within boundaries + } // for y + } // for x + } // lineWidthStyle + } // color + activeMap['points_by_color'] = newPointsByColor + activeMap['stations'] = newStations + } else if (mapDataVersion == 2) { var newPointsByColor = {} var newStations = {} for (var color in activeMap['points_by_color']) { @@ -1947,6 +2648,7 @@ function disableRightClick(event) { function enableRightClick() { document.getElementById('grid-canvas').removeEventListener('contextmenu', disableRightClick); document.getElementById('hover-canvas').removeEventListener('contextmenu', disableRightClick); + document.getElementById('ruler-canvas').removeEventListener('contextmenu', disableRightClick); } // enableRightClick() function getCanvasXY(pageX, pageY) { @@ -1980,13 +2682,41 @@ function getCanvasXY(pageX, pageY) { return [x, y] } // getCanvasXY(pageX, pageY) +function ffSpan(color, initialColor, different) { + // Helper function to help determine the boundaries for floodFill, + // because in JS, ['0896d7', '1-solid'] is not equal to ['0896d7', '1-solid'] + if (different) { + if (color && initialColor && (color[0] != initialColor[0] || color[1] != initialColor[1])) { + return true + } else if (color && !initialColor) { + return true + } else if (!color && initialColor) { + return true + } + return false + } + + if (color === undefined && initialColor === undefined) { + return true + } + if (color && initialColor && color[0] == initialColor[0] && color[1] == initialColor[1]) { + return true + } + return false +} // ffSpan(color, initialColor, different) + function floodFill(x, y, initialColor, replacementColor, hoverIndicator) { // Note: The largest performance bottleneck is actually in // drawing all the points when dealing with large areas // N2H: Right now, this doesn't floodFill along diagonals, // but I also don't want it to bleed beyond the diagonals - if (initialColor == replacementColor) + + // Prevent infinite loops, but allow replacing the same color if the style is different + if (mapDataVersion >= 3 && initialColor && initialColor[0] == replacementColor && initialColor[1] == activeLineWidthStyle) { return + } else if (initialColor == replacementColor && mapDataVersion < 3) { + return + } if (!x || !y) return @@ -2002,48 +2732,97 @@ function floodFill(x, y, initialColor, replacementColor, hoverIndicator) { ctx.clearRect(0, 0, canvas.width, canvas.height) } - while (coords.length > 0) - { - y = coords.pop() - x = coords.pop() - x1 = x; - while(x1 >= 0 && getActiveLine(x1, y, activeMap) == initialColor) - x1--; - x1++; - spanAbove = spanBelow = 0; - while(x1 < gridCols && getActiveLine(x1, y, activeMap) == initialColor) + if (mapDataVersion <= 2) { + // While it might be simpler to have a unified version of this function, + // the differences in the return values of getActiveLine() are significant enough + // among the versions, and this avoids an infinite loop when flood filling over a black line (000000) in v2 + // TODO: Revisit a unified version; might be able to special-case the 000000 problem + while (coords.length > 0) { - if (hoverIndicator) { - if (ignoreCoords && ignoreCoords[x1] && ignoreCoords[x1][y]) - break - if (!ignoreCoords.hasOwnProperty(x1)) - ignoreCoords[x1] = {} - drawHoverIndicator(x1, y, replacementColor, 0.5) - ignoreCoords[x1][y] = true - } else { - updateMapObject(x1, y, "line", replacementColor); - } - if(!spanAbove && y > 0 && getActiveLine(x1, y-1, activeMap) == initialColor) - { - coords.push(x1, y - 1); - spanAbove = 1; - } - else if(spanAbove && y > 0 && getActiveLine(x1, y-1, activeMap) != initialColor) - { - spanAbove = 0; - } - if(!spanBelow && y < gridRows - 1 && getActiveLine(x1, y+1, activeMap) == initialColor) - { - coords.push(x1, y + 1); - spanBelow = 1; - } - else if(spanBelow && y < gridRows - 1 && getActiveLine(x1, y+1, activeMap) != initialColor) + y = coords.pop() + x = coords.pop() + x1 = x; + while(x1 >= 0 && getActiveLine(x1, y, activeMap, mapDataVersion >= 3) == initialColor) + x1--; + x1++; + spanAbove = spanBelow = 0; + while(x1 < gridCols && getActiveLine(x1, y, activeMap, mapDataVersion >= 3) == initialColor) { - spanBelow = 0; - } + if (hoverIndicator) { + if (ignoreCoords && ignoreCoords[x1] && ignoreCoords[x1][y]) + break + if (!ignoreCoords.hasOwnProperty(x1)) + ignoreCoords[x1] = {} + drawHoverIndicator(x1, y, replacementColor, 0.5) + ignoreCoords[x1][y] = true + } else { + updateMapObject(x1, y, "line", replacementColor); + } + if(!spanAbove && y > 0 && getActiveLine(x1, y-1, activeMap, mapDataVersion >= 3) == initialColor) + { + coords.push(x1, y - 1); + spanAbove = 1; + } + else if(spanAbove && y > 0 && getActiveLine(x1, y-1, activeMap, mapDataVersion >= 3) != initialColor) + { + spanAbove = 0; + } + if(!spanBelow && y < gridRows - 1 && getActiveLine(x1, y+1, activeMap, mapDataVersion >= 3) == initialColor) + { + coords.push(x1, y + 1); + spanBelow = 1; + } + else if(spanBelow && y < gridRows - 1 && getActiveLine(x1, y+1, activeMap, mapDataVersion >= 3) != initialColor) + { + spanBelow = 0; + } + x1++; + } // while x1 + } // while coords + } else { + while (coords.length > 0) + { + y = coords.pop() + x = coords.pop() + x1 = x; + while(x1 >= 0 && ffSpan(getActiveLine(x1, y, activeMap, mapDataVersion >= 3), initialColor)) + x1--; x1++; - } // while x1 - } // while coords + spanAbove = spanBelow = 0; + while(x1 < gridCols && ffSpan(getActiveLine(x1, y, activeMap, mapDataVersion >= 3), initialColor)) + { + if (hoverIndicator) { + if (ignoreCoords && ignoreCoords[x1] && ignoreCoords[x1][y]) + break + if (!ignoreCoords.hasOwnProperty(x1)) + ignoreCoords[x1] = {} + drawHoverIndicator(x1, y, replacementColor, 0.5) + ignoreCoords[x1][y] = true + } else { + updateMapObject(x1, y, "line", replacementColor); + } + if(!spanAbove && y > 0 && ffSpan(getActiveLine(x1, y-1, activeMap, mapDataVersion >= 3), initialColor)) + { + coords.push(x1, y - 1); + spanAbove = 1; + } + else if(spanAbove && y > 0 && ffSpan(getActiveLine(x1, y-1, activeMap, mapDataVersion >= 3), initialColor, true)) + { + spanAbove = 0; + } + if(!spanBelow && y < gridRows - 1 && ffSpan(getActiveLine(x1, y+1, activeMap, mapDataVersion >= 3), initialColor)) + { + coords.push(x1, y + 1); + spanBelow = 1; + } + else if(spanBelow && y < gridRows - 1 && ffSpan(getActiveLine(x1, y+1, activeMap, mapDataVersion >= 3), initialColor, true)) + { + spanBelow = 0; + } + x1++; + } // while x1 + } // while coords + } // mapDataVersion check } // floodFill() (scanline implementation) function combineCanvases() { @@ -2072,6 +2851,7 @@ function downloadImage(canvas, showImg) { if (showImg) { $('#grid-canvas').hide(); $('#hover-canvas').hide(); + $('#ruler-canvas').hide(); $('#metro-map-canvas').hide(); $('#metro-map-stations-canvas').hide(); $('#metro-map-image').attr('src', imageData) @@ -2094,6 +2874,7 @@ function downloadImage(canvas, showImg) { if (showImg) { $('#grid-canvas').hide(); $('#hover-canvas').hide(); + $('#ruler-canvas').hide(); $('#metro-map-canvas').hide(); $('#metro-map-stations-canvas').hide(); $('#metro-map-image').attr('src', pngUrl) @@ -2170,15 +2951,11 @@ function resetRailLineTooltips() { } // resetRailLineTooltips function showGrid() { - $('canvas#grid-canvas').removeClass('hide-gridlines'); $('canvas#grid-canvas').css("opacity", 1); - $('#tool-grid span').html('Hide grid'); } function hideGrid() { - $('canvas#grid-canvas').addClass('hide-gridlines'); $('canvas#grid-canvas').css("opacity", 0); - $('#tool-grid span').html('Show grid'); } function setFloodFillUI() { @@ -2204,7 +2981,7 @@ function setFloodFillUI() { indicatorColor = activeToolOption else indicatorColor = '#ffffff' - floodFill(hoverX, hoverY, getActiveLine(hoverX, hoverY, activeMap), indicatorColor, true) + floodFill(hoverX, hoverY, getActiveLine(hoverX, hoverY, activeMap, (mapDataVersion >= 3)), indicatorColor, true) } } else { $('#tool-line-icon-pencil').show() @@ -2267,6 +3044,7 @@ $(document).ready(function() { // Disable right-click on the grid/hover canvases (but not on the map canvas/image) document.getElementById('grid-canvas').addEventListener('contextmenu', disableRightClick); document.getElementById('hover-canvas').addEventListener('contextmenu', disableRightClick); + document.getElementById('ruler-canvas').addEventListener('contextmenu', disableRightClick); // Enable the tooltips $(function () { @@ -2295,30 +3073,31 @@ $(document).ready(function() { return } + // Define keyboard shortcuts here var possibleRailLines = $('.rail-line') var railKey = false - if (event.key == 'z' && (event.metaKey || event.ctrlKey)) { + if (event.key.toLowerCase() == 'z' && (event.metaKey || event.ctrlKey)) { // If Control+Z is pressed undo(); - }if (event.key == 'y' && (event.metaKey || event.ctrlKey)) { + }if (event.key.toLowerCase() == 'y' && (event.metaKey || event.ctrlKey)) { event.preventDefault() // Don't open the History menu - // TODO: Add Redo feature + redo(); } - else if ((event.key == 'c') && (!event.metaKey && !event.ctrlKey)) { // C + else if ((event.key.toLowerCase() == 'c') && (!event.metaKey && !event.altKey && !event.ctrlKey)) { // C if (menuIsCollapsed) { $('#controls-expand-menu').trigger('click') } else { $('#controls-collapse-menu').trigger('click') } } - else if (event.key == 'd') { // D + else if (event.key.toLowerCase() == 'd') { // D $('#tool-line').trigger('click') } - else if (event.key == 'e') { // E + else if (event.key.toLowerCase() == 'e') { // E $('#tool-eraser').trigger('click') } - else if (event.key == 'f') { // F + else if (event.key.toLowerCase() == 'f') { // F if ($('#tool-flood-fill').prop('checked')) { $('#tool-flood-fill').prop('checked', false) } else { @@ -2326,31 +3105,47 @@ $(document).ready(function() { } setFloodFillUI() } - else if (event.key == 'g') { // G + else if (event.key.toLowerCase() == 'g') { // G if ($('#straight-line-assist').prop('checked')) { $('#straight-line-assist').prop('checked', false) } else { $('#straight-line-assist').prop('checked', true) } } - else if (event.key == 'h') { // H + else if (event.key.toLowerCase() == 'h') { // H $('#tool-grid').trigger('click') } - else if (event.key == 's') { // S + else if (event.key.toLowerCase() == 's') { // S $('#tool-station').trigger('click') } - else if ((event.key == 'y') && (!event.metaKey && !event.ctrlKey)) { // Y - if (!menuIsCollapsed) { + else if (event.key.toLowerCase() == 'l') { // L + $('#tool-label').trigger('click') + } + else if ((event.key.toLowerCase() == 'y') && (!event.metaKey && !event.ctrlKey)) { // Y + if (!menuIsCollapsed && mapDataVersion > 1) { $('#tool-map-style').trigger('click') } } - else if (event.key == 'ArrowLeft' && (!event.metaKey && !event.altKey)) { // left arrow, except for "go back" + else if (event.key.toLowerCase() == 'r' && (!event.metaKey && !event.altKey && !event.ctrlKey)) { // R, except for Refresh + $('#tool-ruler').trigger('click') + } + else if (event.key.toLowerCase() == 'w' && (!event.metaKey && !event.altKey && !event.ctrlKey)) { // W, except for close window + if (mapDataVersion >= 3) { + cycleLineWidth() + } + } + else if (event.key.toLowerCase() == 'q' && (!event.metaKey && !event.altKey && !event.ctrlKey)) { // Q, except for quit + if (mapDataVersion >= 3) { + cycleLineStyle() + } + } + else if (event.key == 'ArrowLeft' && (!event.metaKey && !event.altKey && !event.ctrlKey)) { // left arrow, except for "go back" event.preventDefault(); moveMap('left') } else if (event.key == 'ArrowUp') { // up arrow event.preventDefault(); moveMap('up') } - else if (event.key == 'ArrowRight' && (!event.metaKey && !event.altKey)) { // right arrow, except for "go forward" + else if (event.key == 'ArrowRight' && (!event.metaKey && !event.altKey && !event.ctrlKey)) { // right arrow, except for "go forward" event.preventDefault(); moveMap('right') } else if (event.key == 'ArrowDown') { // down arrow @@ -2399,6 +3194,9 @@ $(document).ready(function() { } else if (activeTool == 'station') { $('#tool-station').addClass('active') } + if (!rulerOn && $(this).attr('id') == 'tool-ruler') { + $(this).removeClass('active') + } }) $('#toolbox button.rail-line').on('click', function() { $('.active').removeClass('active') @@ -2424,6 +3222,9 @@ $(document).ready(function() { if (!$('#tool-station').hasClass('width-100')) { $(this).removeClass('width-100') } + // Also reset the + Add New Line button + $('#rail-line-new span').text('Add New Line') + $('#tool-new-line-options').hide() } else { $('#tool-line-options').show(); $(this).addClass('width-100') @@ -2444,7 +3245,7 @@ $(document).ready(function() { $('#tool-line').attr('style', '') $('#tool-line').removeClass('active') } - if (mapDataVersion == 2) { + if (mapDataVersion == 2 || mapDataVersion == 3) { for (var color of allLines) { color = color.id.slice(10, 16) if (!colorInUse(color)) { @@ -2483,6 +3284,15 @@ $(document).ready(function() { } $('.tooltip').hide(); }); // #tool-station.click() + $('#tool-label').on('click', function() { + activeTool = 'label' + $('.active').removeClass('active') + $(this).addClass('active') + if ($('#tool-label-options').is(':visible')) { + $('#tool-label-options').hide() + $(this).removeClass('width-100') + } + }) $('#tool-eraser').on('click', function() { activeTool = 'eraser'; $('.active').removeClass('active') @@ -2500,10 +3310,7 @@ $(document).ready(function() { setFloodFillUI() }); // #tool-eraser.click() $('#tool-grid').click(function() { - if ($('canvas#grid-canvas').hasClass('hide-gridlines')) - showGrid() - else - hideGrid() + cycleGridStep() $('.tooltip').hide() }); // #tool-grid.click() (Toggle grid visibility) $('#tool-zoom-in').click(function() { @@ -2550,21 +3357,21 @@ $(document).ready(function() { $('.tooltip').hide(); }); // #tool-resize-all.click() $('.resize-grid').click(function() { - autoSave(activeMap) var lastToolUsed = activeTool; activeTool = 'resize' size = $(this).attr('id').split('-').slice(2); // Indicate which size the map is now sized to, and reset any other buttons resetResizeButtons(size) - if (activeMap && activeMap['global'] && activeMap['global']['map_size']) { + if (mapDataVersion >= 2) { activeMap['global']['map_size'] = parseInt(size) } setMapSize(activeMap, true) activeTool = lastToolUsed; // Reset tool after drawing the grid to avoid undefined behavior when eraser was the last-used tool + autoSave(activeMap) }); // .resize-grid.click() $('#tool-resize-stretch').click(function() { - autoSave(activeMap) stretchMap() + autoSave(activeMap) }) // #tool-resize-stretch.click() $('#tool-move-all').on('click', function() { if ($('#tool-move-options').is(':visible')) { @@ -2732,6 +3539,7 @@ $(document).ready(function() { if ($('#grid-canvas').is(':visible')) { $('#grid-canvas').hide(); $('#hover-canvas').hide(); + $('#ruler-canvas').hide(); $('#metro-map-canvas').hide(); $('#metro-map-stations-canvas').hide(); var canvas = document.getElementById('metro-map-canvas'); @@ -2750,6 +3558,7 @@ $(document).ready(function() { } else { $('#grid-canvas').show(); $('#hover-canvas').show(); + $('#ruler-canvas').show(); $('#metro-map-canvas').show(); $('#metro-map-stations-canvas').show(); $("#metro-map-image").hide(); @@ -2770,8 +3579,8 @@ $(document).ready(function() { activeMap['global']['map_size'] = 80 - drawGrid() snapCanvasToGrid() + drawGrid() lastStrokeStyle = undefined; drawCanvas(activeMap, false, true) drawCanvas(activeMap, true, true) @@ -2832,7 +3641,7 @@ $(document).ready(function() { $('#tool-new-line-errors').text('Too many rail lines! Delete your unused ones before creating new ones.'); } else { $('#tool-new-line-errors').text(''); - $('#rail-line-new').before(''); + $('#line-color-options fieldset').append(''); if (!activeMap['global']) { activeMap['global'] = {"lines": {}} } @@ -2904,9 +3713,18 @@ $(document).ready(function() { var lineNameToChange = $('#tool-lines-to-change option:selected').text() var lineNameToChangeTo = $('#change-line-name').val().replaceAll('<', '').replaceAll('>', '').replaceAll('"', '').replaceAll('\\\\', '').replaceAll('/', '-') // use same replaces as in $('#create-new-rail-line').click() + var allNames = []; + $('.rail-line').each(function() { + allNames.push($(this).text()); + }); + if ((lineColorToChange != lineColorToChangeTo) && (Object.keys(activeMap["global"]["lines"]).indexOf(lineColorToChangeTo) >= 0)) { $('#cant-save-rail-line-edits').text('Can\'t change ' + lineNameToChange + ' - it has the same color as ' + activeMap["global"]["lines"][lineColorToChangeTo]["displayName"]) - } else { + } + else if (allNames.indexOf(lineNameToChangeTo) > -1 && lineNameToChange != lineNameToChangeTo) { + $('#cant-save-rail-line-edits').text('This rail line name already exists! Please choose a new name.'); + } + else { replaceColors({ "color": lineColorToChange, "name": lineNameToChange @@ -2914,7 +3732,7 @@ $(document).ready(function() { "color": lineColorToChangeTo, "name": lineNameToChangeTo }) - $('#rail-line-change').html('Edit colors & names') + $('#rail-line-change span').html('Edit colors & names') $('#cant-save-rail-line-edits').text('') $('#tool-change-line-options').hide() // If the line tool is in use, unset it so we don't get a stale color @@ -2922,9 +3740,9 @@ $(document).ready(function() { activeTool = 'look' $('#tool-line').attr('style', '') $('#tool-line').removeClass('active') - } - } - } + } // if line + } // else + } // # not Edit which rail line? // If replacing the active line, change the active color too // or you'll end up drawing with a color that no longer exists among the globals if (activeTool == 'line' && rgb2hex(activeToolOption).slice(1, 7) == lineColorToChange) @@ -2961,12 +3779,16 @@ $(document).ready(function() { 'mapLineWidth': mapLineWidth } } + if (mapDataVersion >= 3) { + // Since restyleAllLines calls drawCanvas, I don't need to here. + restyleAllLines(mapLineWidth) + } else { + drawCanvas(activeMap) + } autoSave(activeMap) - drawCanvas() }) $('.map-style-station').on('click', function() { - autoSave(activeMap) mapStationStyle = $(this).data('station-style') $('.map-style-station.active-mapstyle').removeClass('active-mapstyle') $(this).addClass('active-mapstyle') @@ -2989,7 +3811,7 @@ $(document).ready(function() { var y = $('#station-coordinates-y').val(); if (Object.keys(temporaryStation).length > 0) { - if (mapDataVersion == 2) { + if (mapDataVersion == 2 || mapDataVersion == 3) { if (!activeMap["stations"]) { activeMap["stations"] = {} } if (!activeMap["stations"][x]) { activeMap["stations"][x] = {} } activeMap["stations"][x][y] = Object.assign({}, temporaryStation) @@ -3015,7 +3837,7 @@ $(document).ready(function() { if (Object.keys(temporaryStation).length > 0) { temporaryStation["orientation"] = 0 } else { - if (mapDataVersion == 2) + if (mapDataVersion == 2 || mapDataVersion == 3) activeMap["stations"][x][y]["orientation"] = 0 else if (mapDataVersion == 1) activeMap[x][y]["station"]["orientation"] = 0 @@ -3024,7 +3846,7 @@ $(document).ready(function() { if (Object.keys(temporaryStation).length > 0) { temporaryStation["orientation"] = orientation } else { - if (mapDataVersion == 2) + if (mapDataVersion == 2 || mapDataVersion == 3) activeMap["stations"][x][y]["orientation"] = orientation else if (mapDataVersion == 1) activeMap[x][y]["station"]["orientation"] = orientation @@ -3051,13 +3873,13 @@ $(document).ready(function() { if (Object.keys(temporaryStation).length > 0) { temporaryStation["style"] = thisStationStyle } else { - if (mapDataVersion == 2) + if (mapDataVersion == 2 || mapDataVersion == 3) activeMap["stations"][x][y]["style"] = thisStationStyle else if (mapDataVersion == 1) activeMap[x][y]["station"]["style"] = thisStationStyle } // else (not temporaryStation) } else if (!thisStationStyle) { - if (mapDataVersion == 2 && activeMap['stations'][x][y]['style']) { + if ((mapDataVersion == 2 || mapDataVersion == 3) && activeMap['stations'][x][y]['style']) { delete activeMap['stations'][x][y]['style'] } else if (mapDataVersion == 1 && activeMap[x][y]['station']['style']) { delete activeMap[x][y]['station']['style'] @@ -3070,7 +3892,7 @@ $(document).ready(function() { } drawCanvas(activeMap, true); drawIndicator(x, y); - }); // $('#station-name-orientation').change() + }); // $('#station-style').change() $('#station-transfer').click(function() { var x = $('#station-coordinates-x').val(); @@ -3080,7 +3902,7 @@ $(document).ready(function() { if (Object.keys(temporaryStation).length > 0) { temporaryStation["transfer"] = 1 } else { - if (mapDataVersion == 2) + if (mapDataVersion == 2 || mapDataVersion == 3) activeMap["stations"][x][y]["transfer"] = 1 else if (mapDataVersion == 1) activeMap[x][y]["station"]["transfer"] = 1 @@ -3089,7 +3911,7 @@ $(document).ready(function() { if (Object.keys(temporaryStation).length > 0) { delete temporaryStation["transfer"] } else { - if (mapDataVersion == 2) + if (mapDataVersion == 2 || mapDataVersion == 3) delete activeMap["stations"][x][y]["transfer"] else if (mapDataVersion == 1) delete activeMap[x][y]["station"]["transfer"] @@ -3107,7 +3929,7 @@ $(document).ready(function() { }); // document.ready() // Cheat codes / Advanced map manipulations -function getSurroundingLine(x, y, metroMap) { +function getSurroundingLine(x, y, metroMap, returnLineWidthStyle) { // Returns a line color only if x,y has two neighbors // with the same color going in the same direction // Important: this can't return early if there's no point at x, y @@ -3122,18 +3944,18 @@ function getSurroundingLine(x, y, metroMap) { if (getActiveLine(x-1, y, metroMap) && (getActiveLine(x-1, y, metroMap) == getActiveLine(x+1, y, metroMap))) { // Left and right match - return getActiveLine(x-1, y, metroMap); + return getActiveLine(x-1, y, metroMap, returnLineWidthStyle); } else if (getActiveLine(x, y-1, metroMap) && (getActiveLine(x, y-1, metroMap) == getActiveLine(x, y+1, metroMap))) { // Top and bottom match - return getActiveLine(x, y-1, metroMap); + return getActiveLine(x, y-1, metroMap, returnLineWidthStyle); } else if (getActiveLine(x-1, y-1, metroMap) && (getActiveLine(x-1, y-1, metroMap) == getActiveLine(x+1, y+1, metroMap))) { // Diagonal: \ - return getActiveLine(x-1, y-1, metroMap); + return getActiveLine(x-1, y-1, metroMap, returnLineWidthStyle); } else if (getActiveLine(x-1, y+1, metroMap) && (getActiveLine(x-1, y+1, metroMap) == getActiveLine(x+1, y-1, metroMap))) { // Diagonal: / - return getActiveLine(x-1, y+1, metroMap); + return getActiveLine(x-1, y+1, metroMap, returnLineWidthStyle); } -} // getSurroundingLine(x, y, metroMap) +} // getSurroundingLine(x, y, metroMap, returnLineWidthStyle) function setAllStationOrientations(metroMap, orientation) { // Set all station orientations to a certain direction @@ -3141,7 +3963,7 @@ function setAllStationOrientations(metroMap, orientation) { if (ALLOWED_ORIENTATIONS.indexOf(orientation) == -1) return - if (mapDataVersion == 2) { + if (mapDataVersion == 2 || mapDataVersion == 3) { for (var x in metroMap['stations']) { for (var y in metroMap['stations'][x]) { x = parseInt(x); @@ -3159,14 +3981,14 @@ function setAllStationOrientations(metroMap, orientation) { if (Object.keys(metroMap[x][y]).indexOf('station') == -1) continue metroMap[x][y]["station"]["orientation"] = orientation - } - } - } -} + } // for y + } // for x + } // mapDataVersion +} // setAllStationOrientations(metroMap, orientation) $('#set-all-station-name-orientation').on('click', function() { - autoSave(activeMap) var orientation = $('#set-all-station-name-orientation-choice').val() setAllStationOrientations(activeMap, orientation) + autoSave(activeMap) drawCanvas() setTimeout(function() { $('#set-all-station-name-orientation').removeClass('active') @@ -3174,7 +3996,7 @@ $('#set-all-station-name-orientation').on('click', function() { }) function resetAllStationStyles(metroMap) { - if (mapDataVersion == 2) { + if (mapDataVersion == 2 || mapDataVersion == 3) { for (var x in metroMap['stations']) { for (var y in metroMap['stations'][x]) { x = parseInt(x); @@ -3209,21 +4031,28 @@ $('#reset-all-station-styles').on('click', function() { }, 500) }) +function combineLineColorWidthStyle(value) { + if (typeof value === 'object') { // mapDataVersion >= 3 + return value.join('-') + } + return value +} + function getLineDirection(x, y, metroMap) { // Returns which direction this line is going in, // to help draw the positioning of new-style stations x = parseInt(x) y = parseInt(y) - origin = getActiveLine(x, y, metroMap) - NW = getActiveLine(x-1, y-1, metroMap) - NE = getActiveLine(x+1, y-1, metroMap) - SW = getActiveLine(x-1, y+1, metroMap) - SE = getActiveLine(x+1, y+1, metroMap) - N = getActiveLine(x, y-1, metroMap) - E = getActiveLine(x+1, y, metroMap) - S = getActiveLine(x, y+1, metroMap) - W = getActiveLine(x-1, y, metroMap) + origin = combineLineColorWidthStyle(getActiveLine(x, y, metroMap, (mapDataVersion >= 3))) + NW = combineLineColorWidthStyle(getActiveLine(x-1, y-1, metroMap, (mapDataVersion >= 3))) + NE = combineLineColorWidthStyle(getActiveLine(x+1, y-1, metroMap, (mapDataVersion >= 3))) + SW = combineLineColorWidthStyle(getActiveLine(x-1, y+1, metroMap, (mapDataVersion >= 3))) + SE = combineLineColorWidthStyle(getActiveLine(x+1, y+1, metroMap, (mapDataVersion >= 3))) + N = combineLineColorWidthStyle(getActiveLine(x, y-1, metroMap, (mapDataVersion >= 3))) + E = combineLineColorWidthStyle(getActiveLine(x+1, y, metroMap, (mapDataVersion >= 3))) + S = combineLineColorWidthStyle(getActiveLine(x, y+1, metroMap, (mapDataVersion >= 3))) + W = combineLineColorWidthStyle(getActiveLine(x-1, y, metroMap, (mapDataVersion >= 3))) info = { "direction": false, @@ -3490,7 +4319,69 @@ function stretchMap(metroMapObject) { var newMapObject = {}; newMapObject['global'] = Object.assign({}, activeMap['global']) - if (mapDataVersion == 2) { + if (mapDataVersion == 3) { + newMapObject['points_by_color'] = {} + for (var color in metroMapObject['points_by_color']) { + newMapObject['points_by_color'][color] = {} + for (var lineWidthStyle in metroMapObject['points_by_color'][color]) { + newMapObject['points_by_color'][color][lineWidthStyle] = {} + for (var x in metroMapObject['points_by_color'][color][lineWidthStyle]) { + for (var y in metroMapObject['points_by_color'][color][lineWidthStyle][x]) { + x = parseInt(x) + y = parseInt(y) + if (!metroMapObject['points_by_color'][color][lineWidthStyle][x][y]) { + continue + } + if (x * 2 > MAX_MAP_SIZE-1 || y * 2 > MAX_MAP_SIZE-1) { + continue + } + if (!newMapObject['points_by_color'][color][lineWidthStyle].hasOwnProperty(x * 2)) { + newMapObject['points_by_color'][color][lineWidthStyle][x * 2] = {} + } + newMapObject['points_by_color'][color][lineWidthStyle][x * 2][y * 2] = metroMapObject['points_by_color'][color][lineWidthStyle][x][y] + } // for y + } // for x + } // for lineWidthStyle + } // for color + + // Set the gridRows and gridCols + setMapSize(newMapObject) + resetResizeButtons(gridCols) + + newMapObject['stations'] = {} + for (var x in metroMapObject['stations']) { + for (var y in metroMapObject['stations'][x]) { + x = parseInt(x) + y = parseInt(y) + if (x * 2 >= gridRows || y * 2 >= gridCols) { + // Prevent orphaned stations + continue + } + if (!newMapObject['stations'].hasOwnProperty(x * 2)) { + newMapObject['stations'][x * 2] = {} + } + newMapObject['stations'][x * 2][y * 2] = Object.assign({}, metroMapObject['stations'][x][y]) + } + } + + // Fill in the newly created in-between spaces + for (var x=1;x= 2) { + $('#tool-map-style').show() + } + + if (mapDataVersion >= 3) { + $('#line-style-options').show() + } + // ----------------------------------------------- $('#tool-move-all, #tool-resize-all').removeClass('width-100') @@ -3728,18 +4635,108 @@ $('#controls-collapse-menu').on('click', collapseToolbox) $('#controls-expand-menu').on('click', expandToolbox) function colorInUse(color) { - if (!activeMap || !activeMap['points_by_color'] || !activeMap['points_by_color'][color] || !activeMap['points_by_color'][color]['xys']) { + if (!activeMap || !activeMap['points_by_color'] || !activeMap['points_by_color'][color]) { return false } - for (var x in activeMap['points_by_color'][color]['xys']) { - for (var y in activeMap['points_by_color'][color]['xys'][x]) { - if (activeMap['points_by_color'][color]['xys'][x][y] == 1) { - return true - } // if there's actually a point here, not just one that was deleted - } // y - } // x + if (mapDataVersion == 3) { + for (var lineWidthStyle in activeMap['points_by_color'][color]) { + for (var x in activeMap['points_by_color'][color][lineWidthStyle]) { + for (var y in activeMap['points_by_color'][color][lineWidthStyle][x]) { + if (activeMap['points_by_color'][color][lineWidthStyle][x][y]) { + return true + } // if there's actually a point here, not just one that was deleted + } // y + } // x + } // lineWidthStyle + } else if (mapDataVersion == 2) { + if (!activeMap['points_by_color'][color]['xys']) { return } + for (var x in activeMap['points_by_color'][color]['xys']) { + for (var y in activeMap['points_by_color'][color]['xys'][x]) { + if (activeMap['points_by_color'][color]['xys'][x][y]) { + return true + } // if there's actually a point here, not just one that was deleted + } // y + } // x + } // mapDataVersion } // colorInUse(color) +$('.line-style-choice-width').on('click', function() { + $('.line-style-choice-width').removeClass('active') + $('.line-style-choice-width.active-mapstyle').removeClass('active-mapstyle') + $(this).addClass('active-mapstyle') + $(this).addClass('active') + activeLineWidth = $(this).attr('data-linewidth') / 100 + activeLineWidthStyle = activeLineWidth + '-' + activeLineStyle + if (activeToolOption) { + activeTool = 'line' + } +}) + +function cycleLineWidth() { + var currentStep = ALLOWED_LINE_WIDTHS.indexOf(activeLineWidth * 100) + if (currentStep == -1 || currentStep == ALLOWED_LINE_WIDTHS.length-1) { + currentStep = 0 + } else { + currentStep += 1 + } + // This odd construction is because Javascript won't respect the zero after the decimal + var button = $('button[data-linewidth="' + ALLOWED_LINE_WIDTHS[currentStep] + '"]') + if (!button.length) { + button = $('button[data-linewidth="' + ALLOWED_LINE_WIDTHS[currentStep] +'.0"]') + } + button.trigger('click') +} // cycleLineWidth() + +function cycleLineStyle() { + var currentStep = ALLOWED_LINE_STYLES.indexOf(activeLineStyle) + if (currentStep == -1 || currentStep == ALLOWED_LINE_STYLES.length-1) { + currentStep = 0 + } else { + currentStep += 1 + } + $('button[data-linestyle="' + ALLOWED_LINE_STYLES[currentStep] + '"]').trigger('click') +} // cycleLineStyle() + +$('.line-style-choice-style').on('click', function() { + $('.line-style-choice-style').removeClass('active') + $('.line-style-choice-style.active-mapstyle').removeClass('active-mapstyle') + $(this).addClass('active-mapstyle') + $(this).addClass('active') + activeLineStyle = $(this).attr('data-linestyle') + activeLineWidthStyle = activeLineWidth + '-' + activeLineStyle + if (activeToolOption) { + activeTool = 'line' + } +}) + +function setLineStyle(style, ctx) { + var pattern; + if (style == 'solid') { + pattern = [] + ctx.lineCap = 'round' + } else if (style == 'dashed') { + pattern = [gridPixelMultiplier, gridPixelMultiplier * 1.5] + ctx.lineCap = 'square' + } else if (style == 'dotted_dense') { + ctx.lineCap = 'butt' + pattern = [gridPixelMultiplier / 2, gridPixelMultiplier / 4] + } else if (style == 'dotted') { + ctx.lineCap = 'butt' + pattern = [gridPixelMultiplier / 2, gridPixelMultiplier / 2] + } else if (style == 'dense_thick') { + pattern = [2, 2] + ctx.lineCap = 'butt' + } else if (style == 'dense_thin') { + pattern = [1, 1] + ctx.lineCap = 'butt' + } else { + // Safety: fallback to solid + pattern = [] + ctx.lineCap = 'round' + } + ctx.setLineDash(pattern) +} // setLineStyle(style, ctx) + function unfreezeMapControls() { // If #tool-export-canvas (screen size: xs) was clicked, // then the screen resized to a larger breakpoint, @@ -3750,3 +4747,220 @@ function unfreezeMapControls() { } resetTooltipOrientation() } + +// Label Controls +$('#label-text, #label-shape, #label-text-color, #label-bg-color-transparent, #label-bg-color').on('change', function() { + // Whenever any aspect of the label changes, update the map object, reset the temporaryLabel, and draw the canvas. + var labelX = $('#label-coordinates-x').val() + var labelY = $('#label-coordinates-y').val() + + // TOOD: Here's where I'll want to limit the character count for square and circle, + // because it doesn't look good with too many characters crammed in + + temporaryLabel['text'] = $('#label-text').val() + temporaryLabel['shape'] = $('#label-shape').val() + temporaryLabel['text-color'] = $('#label-text-color').val() + if ($('#label-bg-color-transparent').is(':checked')) { + temporaryLabel['bg-color'] = undefined + } else { + temporaryLabel['bg-color'] = $('#label-bg-color').val() + } + activeMap = updateMapObject(labelX, labelY, false, Object.assign({}, temporaryLabel)) + autoSave(activeMap); + temporaryLabel = {} + drawCanvas(activeMap, true) + drawLabelIndicator(labelX, labelY) +}) + +$('#tool-ruler').on('click', function() { + rulerOn = !rulerOn + rulerOrigin = [] + var canvas = document.getElementById('ruler-canvas') + var ctx = canvas.getContext('2d') + ctx.clearRect(0, 0, canvas.width, canvas.height) +}) + +function drawRuler(x, y, replaceOrigin) { + // Displays a hover indicator on the hover canvas at x,y + var canvas = document.getElementById('ruler-canvas') + var ctx = canvas.getContext('2d') + ctx.globalAlpha = 0.33 + ctx.fillStyle = '#2ECC71' + ctx.strokeStyle = '#2ECC71' + var gridPixelMultiplier = canvas.width / gridCols + if (rulerOrigin.length == 0) { + // Just draw the point + ctx.fillRect((x - 0.5) * gridPixelMultiplier, (y - 0.5) * gridPixelMultiplier, gridPixelMultiplier, gridPixelMultiplier) + } else if (rulerOrigin.length > 0) { + // Calculate distance between this and origin + + // Clear the canvas + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // Re-draw the origin + ctx.fillRect((rulerOrigin[0] - 0.5) * gridPixelMultiplier, (rulerOrigin[1] - 0.5) * gridPixelMultiplier, gridPixelMultiplier, gridPixelMultiplier) + + // Draw the new point + ctx.fillRect((x - 0.5) * gridPixelMultiplier, (y - 0.5) * gridPixelMultiplier, gridPixelMultiplier, gridPixelMultiplier) + + // Connect the two points + ctx.beginPath() + ctx.lineWidth = gridPixelMultiplier + ctx.lineCap = 'round' + ctx.moveTo((rulerOrigin[0] * gridPixelMultiplier), (rulerOrigin[1] * gridPixelMultiplier)) + ctx.lineTo(x * gridPixelMultiplier, y * gridPixelMultiplier) + ctx.stroke() + ctx.closePath() + + // Draw the distance near the cursor + ctx.textAlign = 'start' + ctx.font = '700 ' + gridPixelMultiplier + 'px sans-serif' + ctx.globalAlpha = 0.67 + ctx.fillStyle = '#000000' + var pointDistance = '' + var deltaX = Math.abs(rulerOrigin[0] - x) + var deltaY = Math.abs(rulerOrigin[1] - y) + if ((!deltaX && deltaY) || (deltaX && !deltaY)) { + // Straight line along one axis, don't need to show both + pointDistance += (deltaX + deltaY) + } else if (deltaX && deltaY) { + // Show both x, y difference + pointDistance += deltaX + ', ' + deltaY + } + ctx.fillText(pointDistance, (x + 1) * gridPixelMultiplier, (y + 1) * gridPixelMultiplier) + } + if (replaceOrigin) { + // Don't replace the origin on hover + rulerOrigin = [x, y] + } +} // drawRuler(x, y, replaceOrigin) + +function cycleGridStep() { + var GRID_STEPS = [3, 5, 7, false] + var currentStep = GRID_STEPS.indexOf(gridStep) + if (currentStep == -1 || currentStep == GRID_STEPS.length-1) { + gridStep = GRID_STEPS[0] + } else { + gridStep = GRID_STEPS[currentStep + 1] + } + if (!gridStep) { + hideGrid() + } else { + showGrid() + } + window.localStorage.setItem('metroMapGridStep', gridStep); + drawGrid() +} // cycleGridStep() + +$('#tool-undo').on('click', undo) +$('#tool-redo').on('click', redo) + +function restyleAllLines(toWidth, toStyle, deferSave) { + var newMapObject = {"points_by_color": {}} + newMapObject["stations"] = Object.assign({}, activeMap["stations"]) + newMapObject["global"] = Object.assign({}, activeMap["global"]) + for (var color in activeMap["points_by_color"]) { + for (var lws in activeMap["points_by_color"][color]) { + if (mapDataVersion >= 3) { + var lw = lws.split('-')[0] + var ls = lws.split('-')[1] + } + var newLws = (toWidth || lw) + '-' + (toStyle || ls) + if (!newMapObject["points_by_color"][color]) { + newMapObject["points_by_color"][color] = {} + } + if (!newMapObject["points_by_color"][color][newLws]) { + newMapObject["points_by_color"][color][newLws] = {} + } + newMapObject["points_by_color"][color][newLws] = Object.assign( + newMapObject["points_by_color"][color][newLws], + activeMap["points_by_color"][color][lws] + ) + } // lws + } // color + if (mapDataVersion == 2) { + // Upgrade from v2 to v3 is complete + mapDataVersion = 3 + newMapObject["global"]["data_version"] = 3 + compatibilityModeIndicator() + } + activeMap = newMapObject + if (!deferSave) { + autoSave(activeMap) + drawCanvas(activeMap) + } +} // restyleAllLines(toWidth, toStyle, deferSave) + +function upgradeMapDataVersion(desiredMapDataVersion) { + // Upgrades to the highest mapDataVersion possible. + if (mapDataVersion == 1) { + if (desiredMapDataVersion && desiredMapDataVersion == 1) { + // Hide features not yet available + $('#line-style-options').hide() + $('#tool-map-style, #tool-map-style-options').hide() + $('#station-style, label[for="station-style"]').hide() + return + } + var newMapObject = { + "points_by_color": {}, + "stations": {}, + } + newMapObject["global"] = Object.assign({}, activeMap["global"]) + var highestValue = getMapSize(activeMap) || 0 + for (allowedSize of ALLOWED_SIZES) { + if (highestValue < allowedSize) { + gridRows = allowedSize + gridCols = allowedSize + newMapObject["global"]['map_size'] = gridRows + break + } + } + newMapObject["global"]["style"] = { + "mapLineWidth": 1, + "mapLineStyle": 'solid' + } + for (var x in activeMap) { + for (var y in activeMap[x]) { + var color = activeMap[x][y]["line"] + if (!color) { continue } + if (!newMapObject["points_by_color"][color]) { + newMapObject["points_by_color"][color] = {"xys": {}} + } + if (!newMapObject["points_by_color"][color]["xys"][x]) { + newMapObject["points_by_color"][color]["xys"][x] = {} + } + newMapObject["points_by_color"][color]["xys"][x][y] = 1 + if (activeMap[x][y]["station"]) { + if (!newMapObject["stations"][x]) { + newMapObject["stations"][x] = {} + } + newMapObject["stations"][x][y] = Object.assign({}, activeMap[x][y]["station"]) + } // if station + } // for y + } // for x + // Upgrade from v1 to v2 is complete + mapDataVersion = 2 + newMapObject["global"]["data_version"] = 2 + activeMap = Object.assign({}, newMapObject) + } // mapDataVersion 1 + if (mapDataVersion == 2) { + if (desiredMapDataVersion && desiredMapDataVersion == 2) { + // Hide features not yet available + $('#line-style-options').hide() + return + } + var toWidth = 1 + var toStyle = 'solid' + if (activeMap["global"] && activeMap["global"]["style"] && activeMap["global"]["style"]["mapLineWidth"]) { + toWidth = activeMap["global"]["style"]["mapLineWidth"] + } + if (activeMap["global"] && activeMap["global"]["style"] && activeMap["global"]["style"]["mapLineStyle"]) { + toStyle = activeMap["global"]["style"]["mapLineStyle"] + } + restyleAllLines(toWidth, toStyle, true) + } + compatibilityModeIndicator() + // Delete undo/redo history, because I don't want to be able to downgrade to a lower mapDataVersion by undoing + mapHistory = [] + mapRedoHistory = [] +} // upgradeMapDataVersion diff --git a/metro_map_saver/map_saver/static/js/metromapmaker.min.js b/metro_map_saver/map_saver/static/js/metromapmaker.min.js index 333bf224..637a0e5e 100644 --- a/metro_map_saver/map_saver/static/js/metromapmaker.min.js +++ b/metro_map_saver/map_saver/static/js/metromapmaker.min.js @@ -1 +1 @@ -var lastStrokeStyle,gridRows=80,gridCols=80,activeTool="look",activeToolOption=!1,activeMap=!1,preferredGridPixelMultiplier=20,redrawOverlappingPoints={},dragX=!1,dragY=!1,clickX=!1,clickY=!1,hoverX=!1,hoverY=!1,temporaryStation={},pngUrl=!1,mapHistory=[],MAX_UNDO_HISTORY=100,currentlyClickingAndDragging=!1,mapLineWidth=1,mapStationStyle="wmata",menuIsCollapsed=!1,mapSize=void 0,MMMDEBUG=!1;if(void 0===mapDataVersion)var mapDataVersion=void 0;function compatibilityModeIndicator(){1==mapDataVersion?($(".M:not(.mobile)").css({"background-color":"#bd1038"}),$("#title").css({color:"#bd1038"}),$("#tool-move-v1-warning").attr("style","")):($(".M:not(.mobile)").css({"background-color":"#000"}),$("#title").css({color:"#000"}),$("#tool-move-v1-warning").attr("style","display: none"))}compatibilityModeIndicator();const numberKeys=["Digit1","Digit2","Digit3","Digit4","Digit5","Digit6","Digit7","Digit8","Digit9","Digit0","Digit1","Digit2","Digit3","Digit4","Digit5","Digit6","Digit7","Digit8","Digit9","Digit0","Digit1","Digit2","Digit3","Digit4","Digit5","Digit6","Digit7","Digit8","Digit9","Digit0"],ALLOWED_ORIENTATIONS=[0,45,-45,90,-90,135,-135,180],ALLOWED_STYLES=["wmata","rect","rect-round","circles-lg","circles-md","circles-sm","circles-thin"],ALLOWED_SIZES=[80,120,160,200,240,360],MAX_MAP_SIZE=ALLOWED_SIZES[ALLOWED_SIZES.length-1];function resizeGrid(e){if(e=parseInt(e),gridRows=e,gridCols=e,2==mapDataVersion)for(var t in activeMap.points_by_color){for(var i=e;i=e&&activeMap.points_by_color[t].xys[i]&&activeMap.points_by_color[t].xys[i][a]&&delete activeMap.points_by_color[t].xys[i][a],a>=e&&activeMap.stations&&activeMap.stations[i]&&activeMap.stations[i][a]&&delete activeMap.stations[i][a]}else if(1==mapDataVersion){for(i=e;i=e&&activeMap[i]&&activeMap[i][a]&&delete activeMap[i][a]}drawGrid(),snapCanvasToGrid(),lastStrokeStyle=void 0,drawCanvas(activeMap)}function resizeCanvas(e){var t=$("#canvas-container").width();step=gridCols,"out"==e&&t>=800?t-=step:"in"==e&&t<=6400?t+=step:Number.isNaN(e)||(t=parseInt(e)),t<800&&(t=800),t>6400&&(t=6400),window.sessionStorage.setItem("zoomLevel",t),$("#canvas-container").width(t),$("#canvas-container").height(t)}function snapCanvasToGrid(){var e=3600,t=document.getElementById("metro-map-canvas"),i=document.getElementById("metro-map-stations-canvas");t.height/gridCols!=preferredGridPixelMultiplier&&(gridCols*preferredGridPixelMultiplier<=e?(t.height=gridCols*preferredGridPixelMultiplier,i.height=gridCols*preferredGridPixelMultiplier):(t.height=e,i.height=e),gridRows*preferredGridPixelMultiplier<=e?(t.width=gridRows*preferredGridPixelMultiplier,i.width=gridRows*preferredGridPixelMultiplier):(t.width=e,i.width=e)),$("#canvas-container").height($("#metro-map-canvas").height()),$("#canvas-container").width($("#metro-map-canvas").height())}function coordinateInColor(e,t,i,a){return a||1!=mapDataVersion?1==mapDataVersion?getActiveLine(e,t,i)==a:!!(i.points_by_color[a]&&i.points_by_color[a].xys&&i.points_by_color[a].xys[e]&&i.points_by_color[a].xys[e][t]):getActiveLine(e,t,i)}function getActiveLine(e,t,i){if(2==mapDataVersion&&i.global.lines){for(var a in i.points_by_color)if(coordinateInColor(e,t,i,a))return a}else{if(i&&i[e]&&i[e][t]&&i[e][t].line)return i[e][t].line;if(!i)return!1}}function getStation(e,t,i){return i&&2==mapDataVersion&&i.stations&&i.stations[e]?i.stations[e][t]:i&&i[e]&&i[e][t]&&i[e][t].station?i[e][t].station:!!i&&void 0}function moveLineStroke(e,t,i,a,o){e.moveTo(t*gridPixelMultiplier,i*gridPixelMultiplier),e.lineTo(a*gridPixelMultiplier,o*gridPixelMultiplier),singleton=!1}function determineDarkOrLightContrast(e){total=0,rgb=[...e.matchAll(/(\d+)/g)];for(var t=0;t20&&(r=gridPixelMultiplier);for(var e in e=parseInt(e),t=parseInt(t),l.lineWidth=gridPixelMultiplier*mapLineWidth,l.lineCap="round","eraser"==activeTool&&a&&drawPoint(l,e,t,i,a),redrawSection=getRedrawSection(e,t,i,1),redrawSection)for(var t in redrawSection[e])lastStrokeStyle=void 0,e=parseInt(e),t=parseInt(t),"line"==activeTool&&a?drawPoint(l,e,t,i,getActiveLine(e,t,i)):drawPoint(l,e,t,i);if(redrawSection&&!o){var s=(c=document.getElementById("metro-map-stations-canvas")).getContext("2d",{alpha:!0});for(var e in s.font="700 "+r+"px sans-serif",redrawSection)for(var t in redrawSection[e])drawStation(s,e=parseInt(e),t=parseInt(t),i,!0)}else if(o){var c;if((s=(c=document.getElementById("metro-map-stations-canvas")).getContext("2d",{alpha:!0})).clearRect(0,0,c.width,c.height),s.font="700 "+r+"px sans-serif",2==mapDataVersion)for(var e in i.stations)for(var t in i.stations[e])drawStation(s,e=parseInt(e),t=parseInt(t),i);else if(1==mapDataVersion)for(var e in i)for(var t in i[e])e=parseInt(e),t=parseInt(t),Number.isInteger(e)&&Number.isInteger(t)&&drawStation(s,e,t,i)}}function findLines(e){if(activeMap&&activeMap.points_by_color&&activeMap.points_by_color[e]&&activeMap.points_by_color[e].xys){var t={E:new Set,S:new Set,NE:new Set,SE:new Set},i=[],a=new Set,o=new Set;for(var n of["E","S","NE","SE"])for(var l in activeMap.points_by_color[e].xys)for(var r in activeMap.points_by_color[e].xys[l]){var s=l+","+r;if(!t[n].has(s)){var c=findEndpointOfLine(l,r,activeMap.points_by_color[e].xys,n);if(c)for(var p of(i.push([parseInt(l),parseInt(r),c.x1,c.y1]),o.add(s),o.add(c.x1+","+c.y1),c.between))o.add(p),t[n].add(p);else o.has(s)||a.add(s)}}if("function"==typeof a.difference)var d=a.difference(o);else{d=new Set;for(var v of a)o.has(v)||d.add(v)}return{lines:i,singletons:d}}}function findEndpointOfLine(e,t,i,a){var o=[e+","+t];directions={E:{dx:1,dy:0},S:{dx:0,dy:1},NE:{dx:1,dy:-1},SE:{dx:1,dy:1},SW:{dx:-1,dy:1}};var n=directions[a].dx,l=directions[a].dy,r=parseInt(e)+n,s=parseInt(t)+l;if(i&&i[e]&&i[e][t]&&i[r]&&i[r][s]){for(;i[r]&&i[r][s];){o.push(r+","+s);r=parseInt(r)+n,s=parseInt(s)+l}var c=o[o.length-1].split(",");return{between:o,x1:r=parseInt(c[0]),y1:s=parseInt(c[1])}}}function drawCanvas(e,t,i){if(t0=performance.now(),t);else{var a=(f=document.getElementById("metro-map-canvas")).getContext("2d",{alpha:!1});gridPixelMultiplier=Math.floor(f.width/gridCols);var o=gridPixelMultiplier;if(a.fillStyle="#ffffff",a.fillRect(0,0,f.width,f.height),i)return;if(e||(e=activeMap),activeMap=e,a.lineWidth=mapLineWidth*gridPixelMultiplier,a.lineCap="round",2==mapDataVersion)for(var n in e.points_by_color){a.strokeStyle="#"+n;var l=findLines(n),r=l.lines,s=l.singletons;for(var c of r)a.beginPath(),moveLineStroke(a,c[0],c[1],c[2],c[3]),a.stroke(),a.closePath();for(var p of s){var d=p.split(","),v=d[0],g=d[1];a.strokeStyle="#"+n,drawPoint(a,v,g,e,!1,n)}}else if(1==mapDataVersion){for(var v in e)for(var g in e[v])v=parseInt(v),g=parseInt(g),Number.isInteger(v)&&Number.isInteger(g)&&drawPoint(a,v,g,e);for(var m=Object.keys(redrawOverlappingPoints).reverse(),h=0;h0&&"https://metromapmaker.com/"==$.slice(0,26)){var M="Remix this map! Go to "+$;y=a.measureText(M).width;a.fillText(M,gridRows*gridPixelMultiplier-y,gridCols*gridPixelMultiplier-25)}t1=performance.now(),MMMDEBUG&&console.log("drawCanvas finished in "+(t1-t0)+"ms")}function drawPoint(e,t,i,a,o,n,l){if(i=parseInt(i),(n=n||getActiveLine(t,i,a))||"eraser"==activeTool){e.beginPath(),lastStrokeStyle&&lastStrokeStyle==n||(e.strokeStyle="#"+n,lastStrokeStyle=n),o&&(e.strokeStyle="#ffffff",n=o),singleton=!0,coordinateInColor(t+1,i+1,a,n)&&(moveLineStroke(e,t,i,t+1,i+1),redrawOverlappingPoints[t]||(redrawOverlappingPoints[t]={}),redrawOverlappingPoints[t][i]=!0),coordinateInColor(t-1,i-1,a,n)&&moveLineStroke(e,t,i,t-1,i-1),coordinateInColor(t+1,i-1,a,n)&&moveLineStroke(e,t,i,t+1,i-1),coordinateInColor(t-1,i+1,a,n)&&moveLineStroke(e,t,i,t-1,i+1),coordinateInColor(t+1,i,a,n)&&moveLineStroke(e,t,i,t+1,i),coordinateInColor(t-1,i,a,n)&&moveLineStroke(e,t,i,t-1,i),coordinateInColor(t,i+1,a,n)&&moveLineStroke(e,t,i,t,i+1),coordinateInColor(t,i-1,a,n)&&moveLineStroke(e,t,i,t,i-1);var r=getStation(t,i,a);singleton?(e.fillStyle=o?"#ffffff":"#"+n,"rect"==mapStationStyle||r&&"rect"==r.style?e.fillRect((t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,gridPixelMultiplier,gridPixelMultiplier):"circles-md"==mapStationStyle||r&&"circles-md"==r.style?(e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,.7*gridPixelMultiplier,0,2*Math.PI,!0),e.fill()):"circles-sm"==mapStationStyle||r&&"circles-sm"==r.style?(e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,.5*gridPixelMultiplier,0,2*Math.PI,!0),e.fill()):(e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,.9*gridPixelMultiplier,0,2*Math.PI,!0),e.fill())):e.stroke(),e.closePath()}}function drawStation(e,t,i,a,o){var n=getStation(t,i,a);if(n){var l=n.transfer,r=n.style||mapStationStyle,s=!1;if(r&&"wmata"!=r)if("circles-lg"==r){drawStyledStation_WMATA(e,t,i,a,l,"#"+getActiveLine(t,i,a))}else"circles-md"==r?drawCircleStation(e,t,i,a,l,.3,gridPixelMultiplier/2):"circles-sm"==r?drawCircleStation(e,t,i,a,l,.25,gridPixelMultiplier/4):"rect"==r?s=drawStyledStation_rectangles(e,t,i,a,l,0,0):"rect-round"!=r&&"circles-thin"!=r||(s=drawStyledStation_rectangles(e,t,i,a,l,0,0,20));else drawStyledStation_WMATA(e,t,i,a,l);o||drawStationName(e,t,i,a,l,s)}}function drawStationName(e,t,i,a,o,n){e.fillStyle="#000000",e.save();var l=getStation(t,i,a),r=l.name.replaceAll("_"," "),s=parseInt(l.orientation),c=e.measureText(r).width;if(o)var p=1.5*gridPixelMultiplier;else if(n)p=gridPixelMultiplier;else p=.75*gridPixelMultiplier;var d=.25*gridPixelMultiplier;-45==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-45),e.fillText(r,p,d)):45==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*45),e.fillText(r,p,d)):-90==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-90),e.fillText(r,-1*c-p,d)):90==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-90),e.fillText(r,p,d)):135==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-45),e.fillText(r,-1*c-p,d)):-135==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*45),e.fillText(r,-1*c-p,d)):180==s?o?e.fillText(r,t*gridPixelMultiplier-1.5*gridPixelMultiplier-c,i*gridPixelMultiplier+gridPixelMultiplier/4):e.fillText(r,t*gridPixelMultiplier-gridPixelMultiplier-c,i*gridPixelMultiplier+gridPixelMultiplier/4):o?e.fillText(r,t*gridPixelMultiplier+1.5*gridPixelMultiplier,i*gridPixelMultiplier+gridPixelMultiplier/4):e.fillText(r,t*gridPixelMultiplier+gridPixelMultiplier,i*gridPixelMultiplier+gridPixelMultiplier/4),e.restore()}function drawStyledStation_WMATA(e,t,i,a,o,n,l){n||(n="#000000"),l||(l="#ffffff"),o&&(drawCircleStation(e,t,i,a,o,1.2,0,n,0,!0),drawCircleStation(e,t,i,a,o,.9,0,l,0,!0)),drawCircleStation(e,t,i,a,o,.6,0,n,0,!0),drawCircleStation(e,t,i,a,o,.3,0,l,0,!0)}function drawCircleStation(e,t,i,a,o,n,l,r,s,c){o&&!r&&(r="#"+getActiveLine(t,i,a)),!s&&o&&mapLineWidth>=.5&&(s="#ffffff",l=gridPixelMultiplier/2),e.fillStyle=r||"#ffffff",e.beginPath(),e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,gridPixelMultiplier*n,0,2*Math.PI,!0),e.closePath(),c||(e.lineWidth=l,e.strokeStyle=s||"#"+getActiveLine(t,i,a),e.stroke()),e.fill()}function drawStyledStation_rectangles(e,t,i,a,o,n,l,r,s){var c="#"+getActiveLine(t,i,a),p=getLineDirection(t,i,a).direction,d=[],v=!1,g=getConnectedStations(t,i,a),m=getStation(t,i,a),h=gridPixelMultiplier,f=gridPixelMultiplier;if(mapLineWidth>=.5&&"singleton"!=p||"singleton"==p&&("rect-round"==mapStationStyle||m&&"rect-round"==m.style)?(e.strokeStyle="#000000",e.fillStyle="#ffffff"):(e.strokeStyle=c,e.fillStyle=c),!0===g)h=gridPixelMultiplier/2;else if(m&&g&&"singleton"!=g&&"conflicting"!=g)dx=g.x1-g.x0,dy=g.y1-g.y0,h=(Math.abs(dx)+1)*gridPixelMultiplier,f=(Math.abs(dy)+1)*gridPixelMultiplier,v=!0,dx>0&&0==dy?p="horizontal":0==dx&&dy>0?p="vertical":dx>0&&dy>0?(p="diagonal-ne",h=gridPixelMultiplier):dx>0&&dy<0&&(p="diagonal-ne",f=gridPixelMultiplier);else{if(!g&&!s)return;"singleton"==g&&("singleton"==p?(e.strokeStyle="#000000",mapLineWidth<.5&&("rect-round"==mapStationStyle||m&&"rect-round"==m.style)&&(e.fillStyle=c)):o||(h=gridPixelMultiplier/2))}function u(a,o){if(e.save(),e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(o),v&&h>f){var n=h/gridPixelMultiplier;h+=n<4?n*(gridPixelMultiplier/4):n*(gridPixelMultiplier/3)}else if(v&&f>h){n=f/gridPixelMultiplier;f+=n<4?n*(gridPixelMultiplier/4):n*(gridPixelMultiplier/3)}MMMDEBUG&&t==$("#station-coordinates-x").val()&&i==$("#station-coordinates-y").val()&&console.log(`stationSpan: ${n}, w1: ${h} h1: ${f}`),r?primitiveRoundRect(e,...a,h,f,r):(e.strokeRect(...a,h,f),e.fillRect(...a,h,f)),e.restore()}if(v&&!s&&(e.strokeStyle="#000000",e.fillStyle="#ffffff"),(!v||!m&&s)&&("rect-round"==mapStationStyle||m&&"rect-round"==m.style)&&(r=2),m&&"rect"==m.style&&(r=!1),e.lineWidth=o?gridPixelMultiplier/2:gridPixelMultiplier/4,s&&!1===g&&(h>f&&(h=f),v=!0),"conflicting"==g&&(p="singleton",h>f&&(h=f),v=!1),n&&(e.strokeStyle=n),l&&(e.fillStyle=l),MMMDEBUG&&t==$("#station-coordinates-x").val()&&i==$("#station-coordinates-y").val()&&console.log(`xy: ${t},${i}; wh: ${h},${f} xf: ${o} ld: ${p} ra: ${d} cs: ${g} dac: ${v} sC: ${n} fC: ${l}`),!v&&("circles-thin"==mapStationStyle&&m&&!m.style||m&&"circles-thin"==m.style))return s||(n="#000000",l="#ffffff"),void drawCircleStation(e,t,i,activeMap,o,.5,e.lineWidth,l,n);if("singleton"==p||!m&&s&&("horizontal"==p||"vertical"==p))d=[(t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,h,f];else if("horizontal"==p&&(v||o))d=[(t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,h,f];else if("horizontal"!=p||v)if("vertical"==p&&(v||o))d=[(t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,h,f];else if("vertical"!=p||v){if("diagonal-ne"==p&&(v||o))return void u([-.5*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/-4);if("diagonal-ne"==p&&!v)return void u([-.25*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/-4);if("diagonal-se"==p&&(v||o))return void u([-.5*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/4);if("diagonal-se"==p&&!v)return void u([-.25*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/4)}else d=[(t-.5)*gridPixelMultiplier,(i-.25)*gridPixelMultiplier,f,h];else d=[(t-.25)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,h,f];return r?primitiveRoundRect(e,...d,r):(e.strokeRect(...d),e.fillRect(...d)),v}function primitiveRoundRect(e,t,i,a,o,n){a<2*n&&(n=a/2),o<2*n&&(n=o/2),e.beginPath(),e.moveTo(t+n,i),e.arcTo(t+a,i,t+a,i+o,n),e.arcTo(t+a,i+o,t,i+o,n),e.arcTo(t,i+o,t,i,n),e.arcTo(t,i,t+a,i,n),e.stroke(),e.fill(),e.closePath()}function drawIndicator(e,t){var i=document.getElementById("metro-map-stations-canvas"),a=i.getContext("2d",{alpha:!1}),o=i.width/gridCols;if(getActiveLine(e,t,activeMap)){var n=getStation(e,t,activeMap),l=mapStationStyle;temporaryStation.style?l=temporaryStation.style:n&&n.style&&(l=n.style);var r=temporaryStation.transfer||n&&n.transfer;l&&"wmata"!=l&&"circles-lg"!=l?"circles-md"==l?drawCircleStation(a,e,t,activeMap,r,.3,o/2,"#00ff00","#000000"):"circles-sm"==l?drawCircleStation(a,e,t,activeMap,r,.25,o/4,"#00ff00","#000000"):"rect"==l?drawStyledStation_rectangles(a,e,t,activeMap,r,"#000000","#00ff00",!1,!0):"rect-round"!=l&&"circles-thin"!=l||drawStyledStation_rectangles(a,e,t,activeMap,r,"#000000","#00ff00",20,!0):drawStyledStation_WMATA(a,e,t,activeMap,r,"#000000","#00ff00")}}function rgb2hex(e){if(/^#[0-9A-F]{6}$/i.test(e))return e;function t(e){return("0"+parseInt(e).toString(16)).slice(-2)}return"#"+t((e=e.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/))[1])+t(e[2])+t(e[3])}function saveMapHistory(e){mapHistory.length>MAX_UNDO_HISTORY&&mapHistory.shift(),JSON.stringify(e)!=window.localStorage.getItem("metroMap")&&mapHistory.push(JSON.stringify(e)),debugUndoRedo()}function autoSave(e){"object"==typeof e&&(saveMapHistory(activeMap=e),e=JSON.stringify(e)),window.localStorage.setItem("metroMap",e),menuIsCollapsed||($("#autosave-indicator").text("Saving locally ..."),$("#title").hide(),setTimeout((function(){$("#autosave-indicator").text(""),$("#title").show()}),1500))}function undo(){var e=!1;mapHistory.length>1?(mapHistory.pop(),e=mapHistory[mapHistory.length-1]):1==mapHistory.length&&(e=mapHistory[0]),debugUndoRedo(),e&&(window.localStorage.setItem("metroMap",e),$(".rail-line").remove(),loadMapFromObject(e=JSON.parse(e)),setMapSize(e),drawCanvas(e),e.global&&e.global.style?(mapLineWidth=e.global.style.mapLineWidth||mapLineWidth||1,mapStationStyle=e.global.style.mapStationStyle||mapStationStyle||"wmata"):(mapLineWidth=1,mapStationStyle="wmata"),resetResizeButtons(gridCols),resetRailLineTooltips())}function debugUndoRedo(){if(MMMDEBUG){$("#announcement").html("");for(var e=0;e"+t+"

")}}}function getURLParameter(e){return decodeURIComponent((new RegExp("[?|&]"+e+"=([^&;]+?)(&|#|;|$)").exec(location.search)||[null,""])[1].replace(/\+/g,"%20"))||null}function autoLoad(){if("undefined"!=typeof savedMapData)activeMap=savedMapData;else{if(!window.localStorage.getItem("metroMap"))return $.get("/load/2LVHmJ3r").done((function(e){activeMap=e,"[ERROR]"==e.replace(/\s/g,"").slice(0,7)?(drawGrid(),bindRailLineEvents(),drawCanvas()):(setMapStyle(activeMap=JSON.parse(activeMap)),mapDataVersion=activeMap&&activeMap.global&&activeMap.global.data_version?activeMap.global.data_version:1,compatibilityModeIndicator(),mapSize=setMapSize(activeMap,mapDataVersion>1),loadMapFromObject(activeMap),mapHistory.push(JSON.stringify(activeMap)),setTimeout((function(){$("#tool-resize-"+gridRows).text("Initial size ("+gridRows+"x"+gridCols+")")}),1e3))})).fail((function(e){drawGrid(),bindRailLineEvents(),drawCanvas()})),void("undefined"!=typeof autoLoadError&&(autoLoadError+="Loading the default map.",$("#announcement").append('

'+autoLoadError+"

"),setTimeout((function(){$("#autoLoadError").remove()}),3e3)));activeMap=JSON.parse(window.localStorage.getItem("metroMap")),"undefined"!=typeof autoLoadError&&(autoLoadError+="Loading your last-edited map.")}setMapStyle(activeMap),mapDataVersion=activeMap&&activeMap.global&&activeMap.global.data_version?activeMap.global.data_version:1,compatibilityModeIndicator(),mapSize=setMapSize(activeMap,mapDataVersion>1),loadMapFromObject(activeMap),mapHistory.push(JSON.stringify(activeMap)),"undefined"!=typeof autoLoadError&&($("#announcement").append('

'+autoLoadError+"

"),setTimeout((function(){$("#autoLoadError").remove()}),3e3)),setTimeout((function(){$("#tool-resize-"+gridRows).text("Initial size ("+gridRows+"x"+gridCols+")")}),1e3);var e=window.sessionStorage.getItem("zoomLevel");e&&resizeCanvas(e)}function getMapSize(e){var t=0;if("object"!=typeof e&&(e=JSON.parse(e)),2==mapDataVersion)for(var i in e.points_by_color){for(var a in thisColorHighestValue=Math.max(...Object.keys(e.points_by_color[i].xys).map(Number).filter(Number.isInteger).filter((function(t){return Object.keys(e.points_by_color[i].xys[t]).length>0}))),e.points_by_color[i].xys){if(thisColorHighestValue>=ALLOWED_SIZES[ALLOWED_SIZES.length-2]){t=thisColorHighestValue;break}y=Math.max(...Object.keys(e.points_by_color[i].xys[a]).map(Number).filter(Number.isInteger)),y>thisColorHighestValue&&(thisColorHighestValue=y)}thisColorHighestValue>t&&(t=thisColorHighestValue)}else if(1==mapDataVersion)for(var a in t=Math.max(...Object.keys(e).map(Number).filter(Number.isInteger).filter((function(t){return Object.keys(e[t]).length>0}))),e){if(t>=ALLOWED_SIZES[ALLOWED_SIZES.length-2])break;y=Math.max(...Object.keys(e[a]).map(Number).filter(Number.isInteger).filter((function(t){return Object.keys(e[a][t]).length>0}))),y>t&&(t=y)}return t}function setMapSize(e,t){var i=gridRows;if(t)gridRows=e.global.map_size,gridCols=e.global.map_size;else for(allowedSize of(highestValue=getMapSize(e),ALLOWED_SIZES))if(highestValue0?$("#tool-line-options button.original-rail-line").remove():e.global.lines={bd1038:{displayName:"Red Line"},df8600:{displayName:"Orange Line"},f0ce15:{displayName:"Yellow Line"},"00b251":{displayName:"Green Line"},"0896d7":{displayName:"Blue Line"},"662c90":{displayName:"Purple Line"},a2a2a2:{displayName:"Silver Line"},"000000":{displayName:"Logo"},"79bde9":{displayName:"Rivers"},cfe4a7:{displayName:"Parks"}};var i=1;for(var a in e.global.lines)e.global.lines.hasOwnProperty(a)&&null===document.getElementById("rail-line-"+a)&&(keyboardShortcut=i<11?' data-toggle="tooltip" title="Keyboard shortcut: '+numberKeys[i-1].replace("Digit","")+'"':i<21?' data-toggle="tooltip" title="Keyboard shortcut: Shift + '+numberKeys[i-1].replace("Digit","")+'"':i<31?' data-toggle="tooltip" title="Keyboard shortcut: Alt + '+numberKeys[i-1].replace("Digit","")+'"':"",$("#rail-line-new").before('"),i++);$((function(){$('[data-toggle="tooltip"]').tooltip({container:"body"}),bindRailLineEvents(),resetRailLineTooltips(),$(".visible-xs").is(":visible")&&($("#canvas-container").removeClass("hidden-xs"),$("#tool-export-canvas").click(),$("#try-on-mobile").attr("disabled",!1))}))}}function updateMapObject(e,t,i,a){if(activeMap)var o=activeMap;else o=JSON.parse(window.localStorage.getItem("metroMap"));if(2==mapDataVersion&&"line"==activeTool&&(o.points_by_color[a]&&o.points_by_color[a].xys||(o.points_by_color[a]={xys:{}})),"eraser"==activeTool)return 2==mapDataVersion?(a||(a=getActiveLine(e,t,o)),o.points_by_color&&o.points_by_color[a]&&o.points_by_color[a].xys&&o.points_by_color[a].xys[e]&&o.points_by_color[a].xys[e][t]&&delete o.points_by_color[a].xys[e][t],o.stations&&o.stations[e]&&o.stations[e][t]&&delete o.stations[e][t]):1==mapDataVersion&&o[e]&&o[e][t]&&delete o[e][t],o;if(1==mapDataVersion&&(o.hasOwnProperty(e)?o[e].hasOwnProperty(t)||(o[e][t]={}):(o[e]={},o[e][t]={})),2==mapDataVersion&&"line"==activeTool)for(var n in o.points_by_color[a].xys[e]||(o.points_by_color[a].xys[e]={}),o.points_by_color[a].xys[e][t]=1,o.points_by_color)n!=a&&o.points_by_color[n].xys&&o.points_by_color[n].xys[e]&&o.points_by_color[n].xys[e][t]&&delete o.points_by_color[n].xys[e][t];else 1==mapDataVersion&&"line"==activeTool?o[e][t].line=a:"station"==activeTool&&(2==mapDataVersion?(o.stations||(o.stations={}),o.stations[e]||(o.stations[e]={}),o.stations[e][t]||(o.stations[e][t]={}),o.stations[e][t][i]=a):o[e][t].station[i]=a);return o}function moveMap(e){if("station"!=activeTool||!$("#tool-station-options").is(":visible")){var t=0,i=0;if("left"==e)t=-1;else if("right"==e)t=1;else if("down"==e)i=1;else if("up"==e)i=-1;if(2==mapDataVersion){var a={},o={};for(var n in activeMap.points_by_color)for(var l in a[n]={xys:{}},activeMap.points_by_color[n].xys)for(var r in activeMap.points_by_color[n].xys[l])if(l=parseInt(l),r=parseInt(r),Number.isInteger(l)&&Number.isInteger(r)){if(a[n].xys[l+t]||(a[n].xys[l+t]={}),0==l&&"left"==e)return;if(l==gridCols-1&&"right"==e)return;if(0==r&&"up"==e)return;if(r==gridRows-1&&"down"==e)return;0<=l&&l=gridCols&&(r=gridCols-1),s<0?s=0:s>=gridRows&&(s=gridRows-1),[r,s]}function floodFill(e,t,i,a,o){if(i!=a&&e&&t){var n=!1,l=spanBelow=0,r=[e,t],s={};if(o){var c=document.getElementById("hover-canvas");c.getContext("2d").clearRect(0,0,c.width,c.height)}for(;r.length>0;){for(t=r.pop(),n=e=r.pop();n>=0&&getActiveLine(n,t,activeMap)==i;)n--;for(n++,l=spanBelow=0;n0&&getActiveLine(n,t-1,activeMap)==i?(r.push(n,t-1),l=1):l&&t>0&&getActiveLine(n,t-1,activeMap)!=i&&(l=0),!spanBelow&&ti=!0),t))}}function resetTooltipOrientation(){tooltipOrientation="",window.innerWidth<=768?tooltipOrientation="top":$("#snap-controls-right").is(":hidden")?tooltipOrientation="left":tooltipOrientation="right",$(".has-tooltip").each((function(){$(this).data("bs.tooltip")&&($(this).data("bs.tooltip").options.placement=tooltipOrientation)}))}function resetRailLineTooltips(){for(var e=$(".rail-line"),t="",i=0;iHide grid")}function hideGrid(){$("canvas#grid-canvas").addClass("hide-gridlines"),$("canvas#grid-canvas").css("opacity",0),$("#tool-grid span").html("Show grid")}function setFloodFillUI(){if($("#tool-flood-fill").prop("checked"))$("#tool-line-icon-pencil").hide(),$("#tool-line-caption-draw").hide(),$("#tool-line-icon-paint-bucket").show(),$("#tool-eraser-icon-paint-bucket").show(),$("#tool-eraser-icon-eraser").hide(),$("#tool-eraser-caption-eraser").hide(),menuIsCollapsed||($("#tool-line-caption-fill").show(),$("#tool-eraser-caption-fill").show()),("line"==activeTool&&activeToolOption||"eraser"==activeTool)&&(indicatorColor="line"==activeTool&&activeToolOption?activeToolOption:"#ffffff",floodFill(hoverX,hoverY,getActiveLine(hoverX,hoverY,activeMap),indicatorColor,!0));else{$("#tool-line-icon-pencil").show(),$("#tool-eraser-icon-eraser").show(),$("#tool-line-icon-paint-bucket").hide(),$("#tool-line-caption-fill").hide(),menuIsCollapsed||($("#tool-line-caption-draw").show(),$("#tool-eraser-caption-eraser").show()),$("#tool-eraser-icon-paint-bucket").hide(),$("#tool-eraser-caption-fill").hide(),$("#tool-eraser-options").hide();var e=document.getElementById("hover-canvas");e.getContext("2d").clearRect(0,0,e.width,e.height),("line"==activeTool&&activeToolOption||"eraser"==activeTool)&&(indicatorColor="line"==activeTool&&activeToolOption?activeToolOption:"#ffffff",drawHoverIndicator(hoverX,hoverY,indicatorColor))}}function setURLAfterSave(e){window.location.href.split("=")[0]}function getSurroundingLine(e,t,i){return getActiveLine((e=parseInt(e))-1,t=parseInt(t),i)&&getActiveLine(e-1,t,i)==getActiveLine(e+1,t,i)?getActiveLine(e-1,t,i):getActiveLine(e,t-1,i)&&getActiveLine(e,t-1,i)==getActiveLine(e,t+1,i)?getActiveLine(e,t-1,i):getActiveLine(e-1,t-1,i)&&getActiveLine(e-1,t-1,i)==getActiveLine(e+1,t+1,i)?getActiveLine(e-1,t-1,i):getActiveLine(e-1,t+1,i)&&getActiveLine(e-1,t+1,i)==getActiveLine(e+1,t-1,i)?getActiveLine(e-1,t+1,i):void 0}function setAllStationOrientations(e,t){if(t=parseInt(t),-1!=ALLOWED_ORIENTATIONS.indexOf(t))if(2==mapDataVersion)for(var i in e.stations)for(var a in e.stations[i])i=parseInt(i),a=parseInt(a),e.stations[i][a].orientation=t;else if(1==mapDataVersion)for(var i in e)for(var a in e[i])i=parseInt(i),a=parseInt(a),Number.isInteger(i)&&Number.isInteger(a)&&-1!=Object.keys(e[i][a]).indexOf("station")&&(e[i][a].station.orientation=t)}function resetAllStationStyles(e){if(2==mapDataVersion)for(var t in e.stations)for(var i in e.stations[t])t=parseInt(t),i=parseInt(i),e.stations[t][i].style&&delete e.stations[t][i].style;else if(1==mapDataVersion)for(var t in e)for(var i in e[t])t=parseInt(t),i=parseInt(i),Number.isInteger(t)&&Number.isInteger(i)&&-1!=Object.keys(e[t][i]).indexOf("station")&&-1!=Object.keys(e[t][i].station).indexOf("style")&&delete e[t][i].station.style}function getLineDirection(e,t,i){return e=parseInt(e),t=parseInt(t),origin=getActiveLine(e,t,i),NW=getActiveLine(e-1,t-1,i),NE=getActiveLine(e+1,t-1,i),SW=getActiveLine(e-1,t+1,i),SE=getActiveLine(e+1,t+1,i),N=getActiveLine(e,t-1,i),E=getActiveLine(e+1,t,i),S=getActiveLine(e,t+1,i),W=getActiveLine(e-1,t,i),info={direction:!1,endcap:!1},origin?(origin==W&&W==E?info.direction="horizontal":origin==N&&N==S?info.direction="vertical":origin==NW&&NW==SE?info.direction="diagonal-se":origin==SW&&SW==NE?info.direction="diagonal-ne":origin==W||origin==E?(info.direction="horizontal",info.endcap=!0):origin==N||origin==S?(info.direction="vertical",info.endcap=!0):origin==NW||origin==SE?(info.direction="diagonal-se",info.endcap=!0):origin==SW||origin==NE?(info.direction="diagonal-ne",info.endcap=!0):info.direction="singleton",info):info}function getConnectedStations(e,t,i){if(getStation(e=parseInt(e),t=parseInt(t),i)){var a=getStation(e-1,t-1,i),o=getStation(e+1,t-1,i),n=getStation(e-1,t+1,i),l=getStation(e+1,t+1,i),r=getStation(e,t-1,i),s=getStation(e+1,t,i),c=getStation(e,t+1,i),p=getStation(e-1,t,i);if(!(a||o||n||l||r||s||c||p))return"singleton";var d={highest:{E:0,S:0,SE:0,NE:0},E:{},S:{},N:{},W:{},NE:{},SE:{},SW:{},NW:{}};if(s&&u(s)){var v=e,g=t;do{v+=1}while(u(getStation(v,g,i)));d.E={x1:v-1,y1:g},d.highest.E=v-e-1}if(c&&u(c)){v=e,g=t;do{g+=1}while(u(getStation(v,g,i)));d.S={x1:v,y1:g-1},d.highest.S=g-t-1}if(o&&u(o)){v=e,g=t;do{v+=1,g-=1}while(u(getStation(v,g,i)));d.NE={x1:v-1,y1:g+1},d.highest.NE=v-e-1}if(l&&u(l)){v=e,g=t;do{v+=1,g+=1}while(u(getStation(v,g,i)));d.SE={x1:v-1,y1:g-1},d.highest.SE=v-e-1}if(p&&u(p)){v=e,g=t;do{v-=1}while(u(getStation(v,g,i)));d.W={x1:v+1,y1:g},d.E.internal=!0,d.highest.E+=Math.abs(v-e)-1}if(r&&u(r)){v=e,g=t;do{g-=1}while(u(getStation(v,g,i)));d.N={x1:v,y1:g+1},d.S.internal=!0,d.highest.S+=Math.abs(g-t)-1}if(a&&u(a)){v=e,g=t;do{v-=1,g-=1}while(u(getStation(v,g,i)));d.NW={x1:v+1,y1:g+1},d.SE.internal=!0,d.highest.SE+=Math.abs(v-e)-1}if(n&&u(n)){v=e,g=t;do{v-=1,g+=1}while(u(getStation(v,g,i)));d.SW={x1:v+1,y1:g-1},d.NE.internal=!0,d.highest.NE+=Math.abs(v-e)-1}var m=Object.values(d.highest).filter((e=>e>0)),h=Math.max(...Object.values(m));if(0==m.length)return"singleton";if(m.indexOf(h)!=m.lastIndexOf(h))return"conflicting";var f=Object.keys(d.highest).filter((e=>!0!==Number.isNaN(e))).sort((function(e,t){return d.highest[e]-d.highest[t]})).reverse();return!f||!f[0]||!d[f[0]].internal&&{x0:e,y0:t,x1:d[f[0]].x1,y1:d[f[0]].y1}}function u(e){return!!e&&("rect"==e.style||"rect-round"==e.style||"circles-thin"==e.style||!(e.style||"rect"!=mapStationStyle&&"rect-round"!=mapStationStyle&&"circles-thin"!=mapStationStyle))}}function stretchMap(e){e||(e=activeMap);var t={};if(t.global=Object.assign({},activeMap.global),2==mapDataVersion){for(var i in t.points_by_color={},e.points_by_color)for(var a in t.points_by_color[i]={xys:{}},e.points_by_color[i].xys)for(var o in e.points_by_color[i].xys[a])a=parseInt(a),o=parseInt(o),e.points_by_color[i].xys[a][o]&&(2*a>MAX_MAP_SIZE-1||2*o>MAX_MAP_SIZE-1||(t.points_by_color[i].xys.hasOwnProperty(2*a)||(t.points_by_color[i].xys[2*a]={}),t.points_by_color[i].xys[2*a][2*o]=e.points_by_color[i].xys[a][o]));for(var a in setMapSize(t),resetResizeButtons(gridCols),t.stations={},e.stations)for(var o in e.stations[a])a=parseInt(a),o=parseInt(o),2*a>=gridRows||2*o>=gridCols||(t.stations.hasOwnProperty(2*a)||(t.stations[2*a]={}),t.stations[2*a][2*o]=Object.assign({},e.stations[a][o]));for(a=1;a-1?$("#snap-controls-left").hide():$("#snap-controls-right").hide(),$("#toolbox button span.button-label").show(),$("#title, #remix, #credits, #rail-line-new, #rail-line-change, #rail-line-delete, #straight-line-assist-options, #flood-fill-options, #tool-move-all, #tool-resize-all, #tool-map-style").show(),$("#tool-move-all, #tool-resize-all").removeClass("width-100"),$("#tool-flood-fill").prop("checked")?($("#tool-line-caption-draw").hide(),$("#tool-eraser-caption-eraser").hide()):($("#tool-line-caption-fill").hide(),$("#tool-eraser-caption-fill").hide()),1==$("#hide-save-share-url").length&&$("#hide-save-share-url").show(),"Name this map"==$("#name-this-map").text()&&$("#name-map, #name-this-map").show(),$("#controls-collapse-menu").show(),$("#controls-expand-menu").hide()}}function colorInUse(e){if(!(activeMap&&activeMap.points_by_color&&activeMap.points_by_color[e]&&activeMap.points_by_color[e].xys))return!1;for(var t in activeMap.points_by_color[e].xys)for(var i in activeMap.points_by_color[e].xys[t])if(1==activeMap.points_by_color[e].xys[t][i])return!0}function unfreezeMapControls(){window.innerWidth>768&&$("#tool-line").prop("disabled")&&$("#tool-export-canvas").click(),resetTooltipOrientation()}String.prototype.replaceAll=function(e,t){return this.replace(new RegExp(e,"g"),t)},autoLoad(),$(document).ready((function(){document.getElementById("canvas-container").addEventListener("click",bindGridSquareEvents,!1),document.getElementById("canvas-container").addEventListener("mousedown",bindGridSquareMousedown,!1),document.getElementById("canvas-container").addEventListener("mousemove",throttle(bindGridSquareMouseover,1),!1),document.getElementById("canvas-container").addEventListener("mouseup",bindGridSquareMouseup,!1),window.addEventListener("resize",unfreezeMapControls),window.addEventListener("scroll",(function(){$(".tooltip").hide()})),mouseIsDown=!1,document.getElementById("grid-canvas").addEventListener("contextmenu",disableRightClick),document.getElementById("hover-canvas").addEventListener("contextmenu",disableRightClick),$((function(){$('[data-toggle="tooltip"]').tooltip({container:"body"}),window.innerWidth<=768&&$(".has-tooltip").each((function(){$(this).data("bs.tooltip")&&($(this).data("bs.tooltip").options.placement="top")}))})),document.addEventListener("keydown",(function(e){if(document.activeElement&&"text"==document.activeElement.type)"Enter"==e.key&&("new-rail-line-name"==document.activeElement.id?$("#create-new-rail-line").click():"change-line-name"==document.activeElement.id&&$("#save-rail-line-edits").click(),document.activeElement.blur());else{var t=$(".rail-line"),i=!1;"z"==e.key&&(e.metaKey||e.ctrlKey)&&undo(),"y"==e.key&&(e.metaKey||e.ctrlKey)?e.preventDefault():"c"!=e.key||e.metaKey||e.ctrlKey?"d"==e.key?$("#tool-line").trigger("click"):"e"==e.key?$("#tool-eraser").trigger("click"):"f"==e.key?($("#tool-flood-fill").prop("checked")?$("#tool-flood-fill").prop("checked",!1):$("#tool-flood-fill").prop("checked",!0),setFloodFillUI()):"g"==e.key?$("#straight-line-assist").prop("checked")?$("#straight-line-assist").prop("checked",!1):$("#straight-line-assist").prop("checked",!0):"h"==e.key?$("#tool-grid").trigger("click"):"s"==e.key?$("#tool-station").trigger("click"):"y"!=e.key||e.metaKey||e.ctrlKey?"ArrowLeft"!=e.key||e.metaKey||e.altKey?"ArrowUp"==e.key?(e.preventDefault(),moveMap("up")):"ArrowRight"!=e.key||e.metaKey||e.altKey?"ArrowDown"==e.key?(e.preventDefault(),moveMap("down")):"-"==e.key||"_"==e.key?$("#tool-zoom-out").trigger("click"):"="==e.key||"+"==e.key?$("#tool-zoom-in").trigger("click"):"BracketLeft"==e.code?$("#snap-controls-left").trigger("click"):"BracketRight"==e.code?$("#snap-controls-right").trigger("click"):!e.metaKey&&!e.ctrlKey&&e.shiftKey&&numberKeys.indexOf(e.code)>=0?i=numberKeys.indexOf(e.code)+10:!e.metaKey&&!e.ctrlKey&&e.altKey&&numberKeys.indexOf(e.code)>=0?i=numberKeys.indexOf(e.code)+20:!e.metaKey&&!e.ctrlKey&&numberKeys.indexOf(e.code)>=0&&(i=numberKeys.indexOf(e.code)):(e.preventDefault(),moveMap("right")):(e.preventDefault(),moveMap("left")):menuIsCollapsed||$("#tool-map-style").trigger("click"):menuIsCollapsed?$("#controls-expand-menu").trigger("click"):$("#controls-collapse-menu").trigger("click"),!1!==i&&t[i]&&t[i].click()}})),activeTool="look",$("#toolbox button:not(.rail-line)").on("click",(function(){$(".active").removeClass("active"),$(this).addClass("active"),"line"==activeTool?$("#tool-line").addClass("active"):"eraser"==activeTool?$("#tool-eraser").addClass("active"):"station"==activeTool&&$("#tool-station").addClass("active")})),$("#toolbox button.rail-line").on("click",(function(){$(".active").removeClass("active"),$("#tool-line").addClass("active")})),$("#tool-line").on("click",(function(){$(".active").removeClass("active"),$("#tool-line").addClass("active"),$(this).hasClass("draw-rail-line")&&activeToolOption?(activeTool="line",$(this).css({"background-color":activeToolOption})):"eraser"==activeTool&&activeToolOption?activeTool="line":"eraser"==activeTool&&(activeTool="look"),$("#tool-line-options").is(":visible")?($("#tool-line-options").hide(),$("#tool-new-line-options").hide(),$("#tool-station").hasClass("width-100")||$(this).removeClass("width-100")):($("#tool-line-options").show(),$(this).addClass("width-100")),$(".tooltip").hide()})),$("#tool-flood-fill").change((function(){setFloodFillUI()})),$("#rail-line-delete").click((function(){var e=$(".rail-line"),t=[],i=Object.assign({},activeMap);if("line"==activeTool&&(activeTool="look",$("#tool-line").attr("style",""),$("#tool-line").removeClass("active")),2==mapDataVersion)for(var a of e)colorInUse(a=a.id.slice(10,16))||(t.push($("#rail-line-"+a)),delete activeMap.global.lines[a]);else if(1==mapDataVersion){delete i.global,i=JSON.stringify(i);for(var o=0;o0){autoSave(activeMap);for(var n=0;n-1)$("#tool-save-options").html('
You can\'t save an empty map.
'),$("#tool-save-options").show();else if("[ERROR]"==e.replace(/\s/g,"").slice(0,7))$("#tool-save-options").html('
Sorry, there was a problem saving your map: '+e.slice(9)+"
"),console.log("[WARN] Problem was: "+e),$("#tool-save-options").show();else{menuIsCollapsed&&$("#controls-expand-menu").trigger("click");var t=(e=e.split(","))[0].replace(/\s/g,""),i=e[1].replace(/\s/g,""),a='
Map Saved! You can share your map with a friend by using this link: metromapmaker.com/map/'+t+'
You can then share this URL with a friend - and they can remix your map without you losing your original! If you make changes to this map, click Save & Share again to get a new URL.
';i&&(a+='
');var o=window.sessionStorage.getItem("userGivenMapName"),n=window.sessionStorage.getItem("userGivenMapTags");i&&o&&n&&(a+='
Not a map of '+o+"? Click here to rename
"),$("#tool-save-options").html("
Save & Share"+a+"
"),i&&o&&$("#user-given-map-name").val(o),i&&n&&$("#user-given-map-tags").val(n),$("#name-map").submit((function(e){e.preventDefault()})),$("#map-somewhere-else").click((function(){$("#name-map").show(),$("#name-this-map").show(),$(this).parent().hide(),$("#name-this-map").removeClass(),$("#name-this-map").addClass("styling-blueline width-100"),$("#name-this-map").text("Name this map")})),$("#name-this-map").click((function(e){$("#user-given-map-name").val($("#user-given-map-name").val().replaceAll("<","").replaceAll(">","").replaceAll('"',"").replaceAll("\\\\","").replace("&","&").replaceAll("&","&").replaceAll("/","-").replaceAll("'",""));var t=$("#name-map").serializeArray().reduce((function(e,t){return e[t.name]=t.value,e}),{});window.sessionStorage.setItem("userGivenMapName",$("#user-given-map-name").val()),window.sessionStorage.setItem("userGivenMapTags",$("#user-given-map-tags").val()),csrftoken=getCookie("csrftoken"),t.csrfmiddlewaretoken=csrftoken,$.post("/name/",t,(function(){$("#name-map").hide(),$("#name-this-map").text("Thanks!"),setTimeout((function(){$("#name-this-map").hide()}),500)}))})),i&&o&&n&&($("#user-given-map-name").show(),$("#user-given-map-tags").show(),$("#name-this-map").click(),$("#name-this-map").hide()),$("#tool-save-options").show(),$("#hide-save-share-url").click((function(){$("#tool-save-options").hide()}))}})).fail((function(e){if(400==e.status)var t="Sorry, your map could not be saved. Did you flood fill the whole map? Use flood fill with the eraser to erase and try again.";else if(500==e.status)t="Sorry, your map could not be saved right now. This may be a bug, and the admin has been notified.";else if(e.status>=502)t="Sorry, your map could not be saved right now. Metro Map Maker is currently undergoing routine maintenance including bugfixes and upgrades. Please try again in a few minutes.";$("#tool-save-options").html('
'+t+"
"),$("#tool-save-options").show()})).always((function(){setTimeout((function(){$("#tool-save-map span").text("Save & Share")}),350)})),$(".tooltip").hide()})),$("#tool-download-image").click((function(){activeTool="look",downloadImage(combineCanvases()),$(".tooltip").hide()})),$("#tool-export-canvas").click((function(){if(activeTool="look",drawCanvas(activeMap),$("#tool-station-options").hide(),$(".tooltip").hide(),$("#grid-canvas").is(":visible")){$("#grid-canvas").hide(),$("#hover-canvas").hide(),$("#metro-map-canvas").hide(),$("#metro-map-stations-canvas").hide();var e=document.getElementById("metro-map-canvas"),t=document.getElementById("metro-map-stations-canvas");e.getContext("2d",{alpha:!1}).drawImage(t,0,0);var i=e.toDataURL();$("#metro-map-image").attr("src",i),$("#metro-map-image").show(),$("#export-canvas-help").show(),$("button:not(.mobile-browse)").attr("disabled",!0),$(this).attr("disabled",!1),$(this).attr("title","Go back to editing your map").tooltip("fixTitle").tooltip("show")}else $("#grid-canvas").show(),$("#hover-canvas").show(),$("#metro-map-canvas").show(),$("#metro-map-stations-canvas").show(),$("#metro-map-image").hide(),$("#export-canvas-help").hide(),$("button").attr("disabled",!1),$(this).attr("title","Download your map to share with friends").tooltip("fixTitle").tooltip("show")})),$("#tool-clear-map").click((function(){gridRows=80,gridCols=80,(activeMap={global:Object.assign({},activeMap.global),points_by_color:{},stations:{}}).global.map_size=80,drawGrid(),snapCanvasToGrid(),lastStrokeStyle=void 0,drawCanvas(activeMap,!1,!0),drawCanvas(activeMap,!0,!0),window.sessionStorage.removeItem("userGivenMapName"),window.sessionStorage.removeItem("userGivenMapTags"),$(".resize-grid").each((function(){var e=$(this).attr("id").split("-").slice(2),t=e+" x "+e;e==ALLOWED_SIZES[0]&&(t+=" (Current size)"),$(this).html(t)})),showGrid(),$(".tooltip").hide()})),$("#rail-line-new").click((function(){$("#tool-new-line-options").is(":visible")?($(this).children("span").text("Add New Line"),$("#tool-new-line-options").hide()):($(this).children("span").text("Hide Add Line options"),$("#tool-new-line-options").show())})),$("#create-new-rail-line").click((function(){$("#new-rail-line-name").val($("#new-rail-line-name").val().replaceAll("<","").replaceAll(">","").replaceAll('"',"").replaceAll("\\\\","").replaceAll("/","-"));var e=[],t=[];for(var i in $(".rail-line").each((function(){e.push($(this).attr("id").slice(10,16)),t.push($(this).text())})),""==$("#new-rail-line-color").val()&&$("#new-rail-line-color").val("#000000"),e.indexOf($("#new-rail-line-color").val().slice(1,7))>=0?$("#tool-new-line-errors").text("This color already exists! Please choose a new color."):t.indexOf($("#new-rail-line-name").val())>=0?$("#tool-new-line-errors").text("This rail line name already exists! Please choose a new name."):0==$("#new-rail-line-name").val().length?$("#tool-new-line-errors").text("This rail line name cannot be blank. Please enter a name."):$("#new-rail-line-name").val().length>100?$("#tool-new-line-errors").text("This rail line name is too long. Please shorten it."):$(".rail-line").length>99?$("#tool-new-line-errors").text("Too many rail lines! Delete your unused ones before creating new ones."):($("#tool-new-line-errors").text(""),$("#rail-line-new").before('"),activeMap.global||(activeMap.global={lines:{}}),activeMap.global.lines||(activeMap.global.lines={}),$(".rail-line").each((function(){"rail-line-new"!=$(this).attr("id")&&(activeMap.global.lines[$(this).attr("id").slice(10,16)]={displayName:$(this).text()})})),autoSave(activeMap)),bindRailLineEvents(),resetRailLineTooltips(),$("#tool-lines-to-change").html(""),activeMap.global.lines)$("#tool-lines-to-change").append('")})),$("#rail-line-change").click((function(){for(var e in $("#tool-change-line-options").is(":visible")?($(this).children("span").html("Edit colors & names"),$("#tool-change-line-options").hide()):($(this).children("span").text("Close Edit Line options"),$("#tool-change-line-options").show()),$("#tool-lines-to-change").html(""),$("#change-line-name").hide(),$("#change-line-color").hide(),$("#tool-change-line-options label").hide(),$("#tool-change-line-options p").text(""),activeMap.global.lines)$("#tool-lines-to-change").append('")})),$("#tool-lines-to-change").on("change",(function(){$("#tool-change-line-options label").show(),"Edit which rail line?"!=$("#tool-lines-to-change option:selected").text()?($("#change-line-name").show(),$("#change-line-color").show(),$("#change-line-name").val($("#tool-lines-to-change option:selected").text()),$("#change-line-color").val("#"+$(this).val())):($("#tool-change-line-options p").text(""),$("#change-line-name").hide(),$("#change-line-color").hide())})),$("#save-rail-line-edits").click((function(){if("Edit which rail line?"!=$("#tool-lines-to-change option:selected").text()){var e=$("#tool-lines-to-change").val(),t=$("#change-line-color").val().slice(1),i=$("#tool-lines-to-change option:selected").text(),a=$("#change-line-name").val().replaceAll("<","").replaceAll(">","").replaceAll('"',"").replaceAll("\\\\","").replaceAll("/","-");e!=t&&Object.keys(activeMap.global.lines).indexOf(t)>=0?$("#cant-save-rail-line-edits").text("Can't change "+i+" - it has the same color as "+activeMap.global.lines[t].displayName):(replaceColors({color:e,name:i},{color:t,name:a}),$("#rail-line-change").html("Edit colors & names"),$("#cant-save-rail-line-edits").text(""),$("#tool-change-line-options").hide(),"line"==activeTool&&(activeTool="look",$("#tool-line").attr("style",""),$("#tool-line").removeClass("active")))}"line"==activeTool&&rgb2hex(activeToolOption).slice(1,7)==e&&(activeToolOption="#"+t),resetRailLineTooltips()})),$("#tool-map-style").on("click",(function(){$("#tool-map-style-options").toggle(),$("#tool-map-style-options").is(":visible")||$("#tool-map-style").removeClass("active"),$(".tooltip").hide(),.75==mapLineWidth?$("#tool-map-style-line-750").addClass("active-mapstyle"):$("#tool-map-style-line-"+1e3*mapLineWidth).addClass("active-mapstyle"),$("#tool-map-style-station-"+mapStationStyle).addClass("active-mapstyle")})),$(".map-style-line").on("click",(function(){mapLineWidth="tool-map-style-line-750"==$(this).attr("id")?.75:1/parseInt($(this).data("line-width-divisor")),$(".map-style-line.active-mapstyle").removeClass("active-mapstyle"),$(this).addClass("active-mapstyle"),activeMap&&activeMap.global&&activeMap.global.style?activeMap.global.style.mapLineWidth=mapLineWidth:activeMap&&activeMap.global&&(activeMap.global.style={mapLineWidth:mapLineWidth}),autoSave(activeMap),drawCanvas()})),$(".map-style-station").on("click",(function(){autoSave(activeMap),mapStationStyle=$(this).data("station-style"),$(".map-style-station.active-mapstyle").removeClass("active-mapstyle"),$(this).addClass("active-mapstyle"),$("#reset-all-station-styles").text("Set ALL stations to "+$(this).text()),activeMap&&activeMap.global&&activeMap.global.style?activeMap.global.style.mapStationStyle=mapStationStyle:activeMap&&activeMap.global&&(activeMap.global.style={mapStationStyle:mapStationStyle}),autoSave(activeMap),drawCanvas()})),$("#station-name").change((function(){$(this).val($(this).val().replaceAll('"',"").replaceAll("'","").replaceAll("<","").replaceAll(">","").replaceAll("&","").replaceAll("/","").replaceAll("_"," ").replaceAll("\\\\","").replaceAll("%",""));var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val();Object.keys(temporaryStation).length>0&&(2==mapDataVersion?(activeMap.stations||(activeMap.stations={}),activeMap.stations[e]||(activeMap.stations[e]={}),activeMap.stations[e][t]=Object.assign({},temporaryStation)):activeMap[e][t].station=Object.assign({},temporaryStation),temporaryStation={}),metroMap=updateMapObject(e,t,"name",$("#station-name").val()),autoSave(metroMap),drawCanvas(metroMap,!0)})),$("#station-name-orientation").change((function(){var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val(),i=parseInt($(this).val());e>=0&&t>=0&&(0==i?Object.keys(temporaryStation).length>0?temporaryStation.orientation=0:2==mapDataVersion?activeMap.stations[e][t].orientation=0:1==mapDataVersion&&(activeMap[e][t].station.orientation=0):ALLOWED_ORIENTATIONS.indexOf(i)>=0&&(Object.keys(temporaryStation).length>0?temporaryStation.orientation=i:2==mapDataVersion?activeMap.stations[e][t].orientation=i:1==mapDataVersion&&(activeMap[e][t].station.orientation=i))),window.localStorage.setItem("metroMapStationOrientation",i),0==Object.keys(temporaryStation).length&&autoSave(activeMap),drawCanvas(activeMap,!0),drawIndicator(e,t)})),$("#station-style").on("change",(function(){var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val(),i=$(this).val();e>=0&&t>=0&&(ALLOWED_STYLES.indexOf(i)>=0?Object.keys(temporaryStation).length>0?temporaryStation.style=i:2==mapDataVersion?activeMap.stations[e][t].style=i:1==mapDataVersion&&(activeMap[e][t].station.style=i):i||(2==mapDataVersion&&activeMap.stations[e][t].style?delete activeMap.stations[e][t].style:1==mapDataVersion&&activeMap[e][t].station.style&&delete activeMap[e][t].station.style)),0==Object.keys(temporaryStation).length&&autoSave(activeMap),drawCanvas(activeMap,!0),drawIndicator(e,t)})),$("#station-transfer").click((function(){var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val();e>=0&&t>=0&&($(this).is(":checked")?Object.keys(temporaryStation).length>0?temporaryStation.transfer=1:2==mapDataVersion?activeMap.stations[e][t].transfer=1:1==mapDataVersion&&(activeMap[e][t].station.transfer=1):Object.keys(temporaryStation).length>0?delete temporaryStation.transfer:2==mapDataVersion?delete activeMap.stations[e][t].transfer:1==mapDataVersion&&delete activeMap[e][t].station.transfer),0==Object.keys(temporaryStation).length&&autoSave(activeMap),drawCanvas(activeMap,!0),drawIndicator(e,t)})),$("#loading").remove()})),$("#set-all-station-name-orientation").on("click",(function(){autoSave(activeMap);var e=$("#set-all-station-name-orientation-choice").val();setAllStationOrientations(activeMap,e),drawCanvas(),setTimeout((function(){$("#set-all-station-name-orientation").removeClass("active")}),500)})),$("#reset-all-station-styles").on("click",(function(){resetAllStationStyles(activeMap),autoSave(activeMap),drawCanvas(),setTimeout((function(){$("#reset-all-station-styles").removeClass("active")}),500)})),$("#try-on-mobile").click((function(){editOnSmallScreen()})),$("#i-am-on-desktop").on("click",(function(){editOnSmallScreen(),$("#tool-export-canvas").remove(),$("#tool-download-image").removeClass("hidden-xs")})),$("#controls-collapse-menu").on("click",collapseToolbox),$("#controls-expand-menu").on("click",expandToolbox); \ No newline at end of file +var lastStrokeStyle,gridRows=80,gridCols=80,activeTool="look",activeToolOption=!1,activeMap=!1,preferredGridPixelMultiplier=20,redrawOverlappingPoints={},dragX=!1,dragY=!1,clickX=!1,clickY=!1,hoverX=!1,hoverY=!1,temporaryStation={},temporaryLabel={},pngUrl=!1,mapHistory=[],mapRedoHistory=[],MAX_UNDO_HISTORY=100,currentlyClickingAndDragging=!1,mapLineWidth=1,mapLineStyle="solid",activeLineWidth=mapLineWidth,activeLineStyle=mapLineStyle,activeLineWidthStyle=mapLineWidth+"-"+mapLineStyle,mapStationStyle="wmata",menuIsCollapsed=!1,mapSize=void 0,gridStep=5,rulerOn=!1,rulerOrigin=[],MMMDEBUG=!1,MMMDEBUG_UNDO=!1;if(void 0===mapDataVersion)var mapDataVersion=void 0;function compatibilityModeIndicator(){1==mapDataVersion?($(".M:not(.mobile)").css({"background-color":"#bd1038"}),$("#title").css({color:"#bd1038"}),$("#tool-move-v1-warning").attr("style","")):2==mapDataVersion?($(".M:not(.mobile)").css({"background-color":"#df8600"}),$("#title").css({color:"#df8600"}),$("#tool-move-v1-warning").attr("style","display: none")):($(".M:not(.mobile)").css({"background-color":"#000"}),$("#title").css({color:"#000"}),$("#tool-move-v1-warning").attr("style","display: none"))}compatibilityModeIndicator();const numberKeys=["Digit1","Digit2","Digit3","Digit4","Digit5","Digit6","Digit7","Digit8","Digit9","Digit0","Digit1","Digit2","Digit3","Digit4","Digit5","Digit6","Digit7","Digit8","Digit9","Digit0","Digit1","Digit2","Digit3","Digit4","Digit5","Digit6","Digit7","Digit8","Digit9","Digit0"],ALLOWED_LINE_WIDTHS=[100,75,50,25,12.5],ALLOWED_LINE_STYLES=["solid","dashed","dense_thin","dense_thick","dotted_dense","dotted"],ALLOWED_ORIENTATIONS=[0,45,-45,90,-90,135,-135,180,1,-1],ALLOWED_STYLES=["wmata","rect","rect-round","circles-lg","circles-md","circles-sm","circles-thin"],ALLOWED_SIZES=[80,120,160,200,240,360],MAX_MAP_SIZE=ALLOWED_SIZES[ALLOWED_SIZES.length-1];function resizeGrid(e){if(e=parseInt(e),gridRows=e,gridCols=e,3==mapDataVersion)for(var t in activeMap.points_by_color)for(var i in activeMap.points_by_color[t]){for(var a=e;a=e&&activeMap.points_by_color[t][i][a]&&activeMap.points_by_color[t][i][a][o]&&delete activeMap.points_by_color[t][i][a][o],o>=e&&activeMap.stations&&activeMap.stations[a]&&activeMap.stations[a][o]&&delete activeMap.stations[a][o]}else if(2==mapDataVersion)for(var t in activeMap.points_by_color){for(a=e;a=e&&activeMap.points_by_color[t].xys[a]&&activeMap.points_by_color[t].xys[a][o]&&delete activeMap.points_by_color[t].xys[a][o],o>=e&&activeMap.stations&&activeMap.stations[a]&&activeMap.stations[a][o]&&delete activeMap.stations[a][o]}else if(1==mapDataVersion){for(a=e;a=e&&activeMap[a]&&activeMap[a][o]&&delete activeMap[a][o]}for(var t in snapCanvasToGrid(),drawGrid(),lastStrokeStyle=void 0,activeMap.points_by_color)createColorCanvasIfNeeded(t,!0);drawCanvas(activeMap)}function resizeCanvas(e){var t=$("#canvas-container").width();step=gridCols,"out"==e&&t>=800?t-=step:"in"==e&&t<=6400?t+=step:Number.isNaN(e)||(t=parseInt(e)),t<800&&(t=800),t>6400&&(t=6400),window.sessionStorage.setItem("zoomLevel",t),$("#canvas-container").width(t),$("#canvas-container").height(t)}function snapCanvasToGrid(){var e=3600,t=document.getElementById("metro-map-canvas"),i=document.getElementById("metro-map-stations-canvas"),a=document.getElementById("grid-canvas"),o=document.getElementById("hover-canvas"),l=document.getElementById("ruler-canvas");t.height/gridCols!=preferredGridPixelMultiplier&&(gridCols*preferredGridPixelMultiplier<=e?(t.height=gridCols*preferredGridPixelMultiplier,i.height=gridCols*preferredGridPixelMultiplier,a.height=gridCols*preferredGridPixelMultiplier,o.height=gridCols*preferredGridPixelMultiplier,l.height=gridCols*preferredGridPixelMultiplier):(t.height=e,i.height=e,a.height=e,o.height=e,l.height=e),gridRows*preferredGridPixelMultiplier<=e?(t.width=gridRows*preferredGridPixelMultiplier,i.width=gridRows*preferredGridPixelMultiplier,a.width=gridRows*preferredGridPixelMultiplier,o.width=gridRows*preferredGridPixelMultiplier,l.width=gridRows*preferredGridPixelMultiplier):(t.width=e,i.width=e,a.width=e,o.width=e,l.width=e)),$("#canvas-container").height($("#metro-map-canvas").height()),$("#canvas-container").width($("#metro-map-canvas").height())}function coordinateInColor(e,t,i,a,o){if(3==mapDataVersion){if(i.points_by_color[a]&&i.points_by_color[a][o]&&i.points_by_color[a][o][e]&&i.points_by_color[a][o][e][t])return!0}else if(2==mapDataVersion){if(i.points_by_color[a]&&i.points_by_color[a].xys&&i.points_by_color[a].xys[e]&&i.points_by_color[a].xys[e][t])return!0}else if(1==mapDataVersion)return a?getActiveLine(e,t,i)==a:getActiveLine(e,t,i);return!1}function getActiveLine(e,t,i,a){if(3==mapDataVersion&&i.global.lines){for(var o in i.points_by_color)for(var l in i.points_by_color[o])if(coordinateInColor(e,t,i,o,l))return a?[o,l]:o}else if(2==mapDataVersion&&i.global.lines){for(var o in i.points_by_color)if(coordinateInColor(e,t,i,o))return o}else{if(i&&i[e]&&i[e][t]&&i[e][t].line)return i[e][t].line;if(!i)return!1}}function getStation(e,t,i){return i&&(2==mapDataVersion||3==mapDataVersion)&&i.stations&&i.stations[e]?i.stations[e][t]:i&&i[e]&&i[e][t]&&i[e][t].station?i[e][t].station:!!i&&void 0}function getLabel(e,t,i){return i&&(2==mapDataVersion||3==mapDataVersion)&&i.labels&&i.labels[e]?i.labels[e][t]:!!i&&void 0}function moveLineStroke(e,t,i,a,o){e.moveTo(t*gridPixelMultiplier,i*gridPixelMultiplier),e.lineTo(a*gridPixelMultiplier,o*gridPixelMultiplier),singleton=!1}function determineDarkOrLightContrast(e){total=0,rgb=[...e.matchAll(/(\d+)/g)];for(var t=0;t=3),activeToolOption,!0),$("#tool-line-icon-pencil").hide(),$("#tool-line-icon-paint-bucket").show()):(drawNewHoverIndicator(),$("#tool-line-icon-pencil").show(),$("#tool-line-icon-paint-bucket").hide()),$("#tool-station-options").hide()}))}function makeLine(e,t,i){if(1==mapDataVersion)drawArea(e,t,activeMap,!0);else if(mapDataVersion>=2)var a=getActiveLine(e,t,activeMap);var o=rgb2hex(activeToolOption).slice(1,7);metroMap=updateMapObject(e,t,"line",o),i||autoSave(metroMap),mapDataVersion>=2?(a&&redrawCanvasForColor(a),redrawCanvasForColor(o)):1==mapDataVersion&&drawArea(e,t,activeMap)}function redrawCanvasForColor(e){var t=performance.now();drawCanvas(!1,!1,!0),drawColor(e);var i=document.getElementById("metro-map-canvas").getContext("2d",{alpha:!0});for(var e in activeMap.points_by_color){var a=createColorCanvasIfNeeded(e);i.drawImage(a,0,0)}drawCanvas(activeMap,!0);var o=performance.now();MMMDEBUG&&console.log("redrawCanvasForColor finished in "+(o-t)+"ms")}function makeStation(e,t){if(temporaryStation={},!getActiveLine(e,t,activeMap))return activeToolOption?$("#tool-station").removeClass("width-100"):($("#tool-station").addClass("active"),$("#tool-line").removeClass("width-100"),$("#tool-line").removeClass("active")),$("#tool-station-options").hide(),void drawCanvas(activeMap,!0);$("#station-name").val(""),$("#station-coordinates-x").val(e),$("#station-coordinates-y").val(t);$(".rail-line");if($("#tool-station").addClass("width-100"),getStation(e,t,activeMap)){if(getStation(e,t,activeMap).name){var i=getStation(e,t,activeMap).name.replaceAll("_"," ");$("#station-name").val(i),getStation(e,t,activeMap).transfer?$("#station-transfer").prop("checked",!0):$("#station-transfer").prop("checked",!1),document.getElementById("station-name-orientation").value=parseInt(getStation(e,t,activeMap).orientation),document.getElementById("station-style").value=getStation(e,t,activeMap).style||""}}else{temporaryStation={name:""},$("#station-transfer").prop("checked",!1);var a=window.localStorage.getItem("metroMapStationOrientation");a?(document.getElementById("station-name-orientation").value=a,$("#station-name-orientation").change()):document.getElementById("station-name-orientation").value=0,document.getElementById("station-style").value=""}getActiveLine(e,t,activeMap)&&(drawCanvas(activeMap,!0),drawIndicator(e,t),$("#tool-station-options").show()),$("#station-name").focus()}function makeLabel(e,t){temporaryLabel={},$("#tool-label-options").show(),$("#label-coordinates-x").val(e),$("#label-coordinates-y").val(t);var i=getLabel(e,t,activeMap);if(i)i.text&&$("#label-text").val(i.text.replaceAll("_"," ")),i.shape&&$("#label-shape").val(i.shape),i["text-color"]&&$("#label-text-color").val(i["text-color"]),i["bg-color"]?$("#label-bg-color").val(i["bg-color"]):($("#label-bg-color").hide(),$("#label-bg-color-transparent").prop("checked",!0));else{temporaryLabel={text:"",shape:"","text-color":"","bg-color":""},$("#label-text").val("");var a=window.localStorage.getItem("lastLabelShape");a&&(document.getElementById("label-shape").value=a,$("#label-shape").trigger("change"))}drawCanvas(activeMap,!0),drawLabelIndicator(e,t),$("#label-text").focus()}function bindGridSquareEvents(e){if($("#station-coordinates-x").val(""),$("#station-coordinates-y").val(""),e.isTrusted)t=getCanvasXY(e.pageX,e.pageY);else var t=getCanvasXY(dragX,dragY);var i=t[0],a=t[1];if(e.isTrusted){if(currentlyClickingAndDragging)return}else{if(i==clickX||a==clickY);else if(Math.abs(i-clickX)==Math.abs(a-clickY));else if($("#straight-line-assist").prop("checked"))return;currentlyClickingAndDragging=!0}if("line"==activeTool)if($("#tool-flood-fill").prop("checked")){var o=getActiveLine(i,a,activeMap),l=rgb2hex(activeToolOption).slice(1,7);floodFill(i,a,getActiveLine(i,a,activeMap,mapDataVersion>=3),l),autoSave(activeMap),mapDataVersion>=2?(drawColor(o),redrawCanvasForColor(l)):1==mapDataVersion&&drawCanvas(activeMap)}else makeLine(i,a);else if("eraser"==activeTool){var r=getActiveLine(i,a,activeMap);if(!r)return;if(getStation(i,a,activeMap)||getLabel(i,a,activeMap))var n=!0;else n=!1;if(getLabel(i,a,activeMap));else;r&&$("#tool-flood-fill").prop("checked")?(floodFill(i,a,getActiveLine(i,a,activeMap,mapDataVersion>=3),""),autoSave(activeMap),mapDataVersion>=2?redrawCanvasForColor(r):1==mapDataVersion&&drawCanvas(activeMap)):(metroMap=updateMapObject(i,a),autoSave(metroMap),mapDataVersion>=2?redrawCanvasForColor(r):1==mapDataVersion&&drawArea(i,a,metroMap,r,n))}else"station"==activeTool?makeStation(i,a):"label"==activeTool&&makeLabel(i,a)}function bindGridSquareMouseover(e){if($("#ruler-xy").text(getCanvasXY(e.pageX,e.pageY)),xy=getCanvasXY(e.pageX,e.pageY),hoverX=xy[0],hoverY=xy[1],mouseIsDown||$("#tool-flood-fill").prop("checked")){if(!mouseIsDown&&(activeToolOption||"eraser"==activeTool)&&$("#tool-flood-fill").prop("checked")){if("line"==activeTool&&activeToolOption)indicatorColor=activeToolOption;else{if("line"!=activeTool&&"eraser"!=activeTool)return void drawHoverIndicator(e.pageX,e.pageY);indicatorColor="#ffffff"}floodFill(hoverX,hoverY,getActiveLine(hoverX,hoverY,activeMap,mapDataVersion>=3),indicatorColor,!0)}}else drawHoverIndicator(e.pageX,e.pageY),rulerOn&&rulerOrigin.length>0&&("look"==activeTool||"line"==activeTool||"eraser"==activeTool)&&drawRuler(hoverX,hoverY);!mouseIsDown||"line"!=activeTool&&"eraser"!=activeTool||(dragX=e.pageX,dragY=e.pageY,$("#canvas-container").click()),mouseIsDown&&rulerOn&&rulerOrigin.length>0&&("look"==activeTool||"line"==activeTool||"eraser"==activeTool)&&drawRuler(hoverX,hoverY)}function bindGridSquareMouseup(e){if("station"==activeTool&&"text"!=document.activeElement.type&&$("#station-name").focus(),clickX=!1,clickY=!1,mouseIsDown=!1,drawHoverIndicator(e.pageX,e.pageY),rulerOn&&rulerOrigin.length>0&&("line"==activeTool||"eraser"==activeTool)){rulerOrigin=[];var t=document.getElementById("ruler-canvas");t.getContext("2d").clearRect(0,0,t.width,t.height)}}function bindGridSquareMousedown(e){if(xy=getCanvasXY(e.pageX,e.pageY),clickX=xy[0],clickY=xy[1],$("#straight-line-assist").prop("checked")&&("line"==activeTool||"eraser"==activeTool))for(var t=0;t0)gridCols<=240?(o=gridPixelMultiplier/2,i<10?a=i*gridPixelMultiplier+gridPixelMultiplier/4+2:i<100?a=i*gridPixelMultiplier+gridPixelMultiplier/4:i>=100&&(a=i*gridPixelMultiplier+gridPixelMultiplier/4-4)):gridCols>240&&(o=gridPixelMultiplier/1.25,i<10?a=i*gridPixelMultiplier+gridPixelMultiplier/4:i<100?a=i*gridPixelMultiplier+gridPixelMultiplier/4-3:i<1e3&&(a=i*gridPixelMultiplier+gridPixelMultiplier/4-6)),t.fillText(i,a,o),t.fillText(i,a,e.height-gridPixelMultiplier/4)}else t.strokeStyle="#80CEFF";t.beginPath(),t.moveTo(i*gridPixelMultiplier+gridPixelMultiplier/2,0),t.lineTo(i*gridPixelMultiplier+gridPixelMultiplier/2,e.height),t.stroke(),t.closePath()}for(var l=0;l0&&t.fillText(l,e.width-r.width-gridPixelMultiplier/4,l*gridPixelMultiplier+gridPixelMultiplier/2+3)}else t.strokeStyle="#80CEFF";t.beginPath(),t.moveTo(0,l*gridPixelMultiplier+gridPixelMultiplier/2),t.lineTo(e.width,l*gridPixelMultiplier+gridPixelMultiplier/2),t.stroke(),t.closePath()}gridStep?$("#grid-step").text(gridStep):$("#grid-step").text("off")}function getRedrawSection(e,t,i,a){redrawSection={};for(var o=-1*(a=parseInt(a));o<=a;o+=1)for(var l=-1*a;l<=a;l+=1)getActiveLine(e+o,t+l,i)&&(redrawSection.hasOwnProperty(e+o)||(redrawSection[e+o]={}),redrawSection[e+o][t+l]=!0);return redrawSection}function drawArea(e,t,i,a,o){var l=document.getElementById("metro-map-canvas"),r=l.getContext("2d",{alpha:!1});gridPixelMultiplier=l.width/gridCols,fontSize=gridPixelMultiplier;for(var e in e=parseInt(e),t=parseInt(t),r.lineWidth=gridPixelMultiplier*activeLineWidth,r.lineCap="round","eraser"==activeTool&&a&&drawPoint(r,e,t,i,a),redrawSection=getRedrawSection(e,t,i,1),redrawSection)for(var t in redrawSection[e])lastStrokeStyle=void 0,e=parseInt(e),t=parseInt(t),"line"==activeTool&&a?drawPoint(r,e,t,i,getActiveLine(e,t,i)):drawPoint(r,e,t,i);if(redrawSection&&!o){var n=(s=document.getElementById("metro-map-stations-canvas")).getContext("2d",{alpha:!0});for(var e in n.font="700 "+fontSize+"px sans-serif",redrawSection)for(var t in redrawSection[e])drawStation(n,e=parseInt(e),t=parseInt(t),i,!0),drawLabel(n,e,t,i)}else if(o){var s;if((n=(s=document.getElementById("metro-map-stations-canvas")).getContext("2d",{alpha:!0})).clearRect(0,0,s.width,s.height),n.font="700 "+fontSize+"px sans-serif",2==mapDataVersion||3==mapDataVersion){for(var e in i.stations)for(var t in i.stations[e])drawStation(n,e=parseInt(e),t=parseInt(t),i);for(var e in i.labels)for(var t in i.labels[e])drawLabel(n,e=parseInt(e),t=parseInt(t),i)}else if(1==mapDataVersion)for(var e in i)for(var t in i[e])e=parseInt(e),t=parseInt(t),Number.isInteger(e)&&Number.isInteger(t)&&drawStation(n,e,t,i)}}function drawColor(e){if(e){var t=createColorCanvasIfNeeded(e),i=t.getContext("2d",{alpha:!0});if(i.clearRect(0,0,t.width,t.height),3==mapDataVersion)for(var a in activeMap.points_by_color[e]){i.strokeStyle="#"+e;var o=a.split("-")[0]*gridPixelMultiplier,l=a.split("-")[1],r=(v=findLines(e,a)).lines,n=v.singletons;for(var s of r)i.beginPath(),i.lineWidth=o,setLineStyle(l,i),moveLineStroke(i,s[0],s[1],s[2],s[3]),i.stroke(),i.closePath();for(var c of n){var p=(g=c.split(","))[0],d=g[1];i.strokeStyle="#"+e,drawPoint(i,p,d,activeMap,!1,e,o,l)}}else if(2==mapDataVersion){i.strokeStyle="#"+e,activeMap&&activeMap.global&&activeMap.global.style?i.lineWidth=(activeMap.global.style.mapLineWidth||1)*gridPixelMultiplier:i.lineWidth=mapLineWidth*gridPixelMultiplier,i.lineCap="round";var v;r=(v=findLines(e)).lines,n=v.singletons;for(var s of r)i.beginPath(),moveLineStroke(i,s[0],s[1],s[2],s[3]),i.stroke(),i.closePath();for(var c of n){var g;p=(g=c.split(","))[0],d=g[1];i.strokeStyle="#"+e,drawPoint(i,p,d,activeMap,!1,e)}}}}function createColorCanvasIfNeeded(e,t){if(!(i=document.getElementById("metro-map-color-canvas-"+e))){var i,a=document.getElementById("metro-map-canvas"),o=document.getElementById("color-canvas-container");(i=document.createElement("canvas")).id="metro-map-color-canvas-"+e,i.classList="hidden",i.width=a.width,i.height=a.height,o.appendChild(i)}if(t){a=document.getElementById("metro-map-canvas");i.width=a.width,i.height=a.height}return i}function findLines(e,t){if(2!=mapDataVersion||t||(t="xys"),2!=mapDataVersion&&3!=mapDataVersion||activeMap&&activeMap.points_by_color&&activeMap.points_by_color[e]&&activeMap.points_by_color[e][t]){var i={E:new Set,S:new Set,NE:new Set,SE:new Set},a=[],o=new Set,l=new Set;for(var r of["E","S","NE","SE"])for(var n in activeMap.points_by_color[e][t])for(var s in activeMap.points_by_color[e][t][n]){var c=n+","+s;if(!i[r].has(c)){var p=findEndpointOfLine(n,s,activeMap.points_by_color[e][t],r);if(p)for(var d of(a.push([parseInt(n),parseInt(s),p.x1,p.y1]),l.add(c),l.add(p.x1+","+p.y1),p.between))l.add(d),i[r].add(d);else l.has(c)||o.add(c)}}if("function"==typeof o.difference)var v=o.difference(l);else{v=new Set;for(var g of o)l.has(g)||v.add(g)}return{lines:a,singletons:v}}}function findEndpointOfLine(e,t,i,a){var o=[e+","+t];directions={E:{dx:1,dy:0},S:{dx:0,dy:1},NE:{dx:1,dy:-1},SE:{dx:1,dy:1},SW:{dx:-1,dy:1}};var l=directions[a].dx,r=directions[a].dy,n=parseInt(e)+l,s=parseInt(t)+r;if(i&&i[e]&&i[e][t]&&i[n]&&i[n][s]){for(;i[n]&&i[n][s];){o.push(n+","+s);n=parseInt(n)+l,s=parseInt(s)+r}var c=o[o.length-1].split(",");return{between:o,x1:n=parseInt(c[0]),y1:s=parseInt(c[1])}}}function drawCanvas(e,t,i){if(t0=performance.now(),t);else{var a=(d=document.getElementById("metro-map-canvas")).getContext("2d",{alpha:!1});gridPixelMultiplier=Math.floor(d.width/gridCols);var o=gridPixelMultiplier;if(a.fillStyle="#ffffff",a.fillRect(0,0,d.width,d.height),i)return;if(e||(e=activeMap),activeMap=e,a.lineWidth=mapLineWidth*gridPixelMultiplier,a.lineCap="round",mapDataVersion>=2)for(var l in e.points_by_color){drawColor(l);var r=document.getElementById("metro-map-color-canvas-"+l);a.drawImage(r,0,0)}else if(1==mapDataVersion){for(var n in e)for(var s in e[n])n=parseInt(n),s=parseInt(s),Number.isInteger(n)&&Number.isInteger(s)&&drawPoint(a,n,s,e);for(var c=Object.keys(redrawOverlappingPoints).reverse(),p=0;p0&&"https://metromapmaker.com/"==m.slice(0,26)){var h="Remix this map! Go to "+m;g=a.measureText(h).width;a.fillText(h,gridRows*gridPixelMultiplier-g,gridCols*gridPixelMultiplier-25)}t1=performance.now(),MMMDEBUG&&console.log("drawCanvas(map, "+t+", "+i+") finished in "+(t1-t0)+"ms")}function drawPoint(e,t,i,a,o,l,r,n){if(t=parseInt(t),i=parseInt(i),(l=l||getActiveLine(t,i,a))||"eraser"==activeTool){if(e.beginPath(),lastStrokeStyle&&lastStrokeStyle==l||(e.strokeStyle="#"+l,lastStrokeStyle=l),o?(e.strokeStyle="#ffffff",l=o):(e.lineWidth=gridPixelMultiplier*(r||activeLineWidth),setLineStyle(n||activeLineStyle,e)),singleton=!0,r&&n)var s=r+"-"+n;else s=activeLineWidthStyle;coordinateInColor(t+1,i+1,a,l,s)&&(moveLineStroke(e,t,i,t+1,i+1),redrawOverlappingPoints[t]||(redrawOverlappingPoints[t]={}),redrawOverlappingPoints[t][i]=!0),coordinateInColor(t-1,i-1,a,l,s)&&moveLineStroke(e,t,i,t-1,i-1),coordinateInColor(t+1,i-1,a,l,s)&&moveLineStroke(e,t,i,t+1,i-1),coordinateInColor(t-1,i+1,a,l,s)&&moveLineStroke(e,t,i,t-1,i+1),coordinateInColor(t+1,i,a,l,s)&&moveLineStroke(e,t,i,t+1,i),coordinateInColor(t-1,i,a,l,s)&&moveLineStroke(e,t,i,t-1,i),coordinateInColor(t,i+1,a,l,s)&&moveLineStroke(e,t,i,t,i+1),coordinateInColor(t,i-1,a,l,s)&&moveLineStroke(e,t,i,t,i-1);var c=getStation(t,i,a);singleton?(e.fillStyle=o?"#ffffff":"#"+l,"rect"==mapStationStyle||c&&"rect"==c.style?e.fillRect((t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,gridPixelMultiplier,gridPixelMultiplier):"circles-md"==mapStationStyle||c&&"circles-md"==c.style?(e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,.7*gridPixelMultiplier,0,2*Math.PI,!0),e.fill()):"circles-sm"==mapStationStyle||c&&"circles-sm"==c.style?(e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,.5*gridPixelMultiplier,0,2*Math.PI,!0),e.fill()):(e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,.9*gridPixelMultiplier,0,2*Math.PI,!0),e.fill())):e.stroke(),e.closePath()}}function drawStation(e,t,i,a,o){var l=getStation(t,i,a);if(l){var r=l.transfer,n=l.style||mapStationStyle,s=!1;if(n&&"wmata"!=n)if("circles-lg"==n){drawStyledStation_WMATA(e,t,i,a,r,"#"+getActiveLine(t,i,a))}else"circles-md"==n?drawCircleStation(e,t,i,a,r,.3,gridPixelMultiplier/2):"circles-sm"==n?drawCircleStation(e,t,i,a,r,.25,gridPixelMultiplier/4):"rect"==n?s=drawStyledStation_rectangles(e,t,i,a,r,0,0):"rect-round"!=n&&"circles-thin"!=n||(s=drawStyledStation_rectangles(e,t,i,a,r,0,0,20));else drawStyledStation_WMATA(e,t,i,a,r);o||drawStationName(e,t,i,a,r,s)}}function drawStationName(e,t,i,a,o,l){e.textAlign="start",e.fillStyle="#000000",e.save();var r=getStation(t,i,a),n=r.name.replaceAll("_"," "),s=parseInt(r.orientation),c=e.measureText(n).width;if(o)var p=1.5*gridPixelMultiplier;else if(l)p=gridPixelMultiplier;else p=.75*gridPixelMultiplier;var d=.25*gridPixelMultiplier;-45==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-45),e.fillText(n,p,d)):45==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*45),e.fillText(n,p,d)):-90==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-90),e.fillText(n,-1*c-p,d)):90==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-90),e.fillText(n,p,d)):135==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*-45),e.fillText(n,-1*c-p,d)):-135==s?(e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(Math.PI/180*45),e.fillText(n,-1*c-p,d)):180==s?o?e.fillText(n,t*gridPixelMultiplier-1.5*gridPixelMultiplier-c,i*gridPixelMultiplier+gridPixelMultiplier/4):e.fillText(n,t*gridPixelMultiplier-gridPixelMultiplier-c,i*gridPixelMultiplier+gridPixelMultiplier/4):1==s?(e.textAlign="center",o?e.fillText(n,t*gridPixelMultiplier,(i-1.5)*gridPixelMultiplier):e.fillText(n,t*gridPixelMultiplier,(i-1)*gridPixelMultiplier)):-1==s?(e.textAlign="center",o?e.fillText(n,t*gridPixelMultiplier,(i+2.25)*gridPixelMultiplier):e.fillText(n,t*gridPixelMultiplier,(i+1.75)*gridPixelMultiplier)):o?e.fillText(n,t*gridPixelMultiplier+1.5*gridPixelMultiplier,i*gridPixelMultiplier+gridPixelMultiplier/4):e.fillText(n,t*gridPixelMultiplier+gridPixelMultiplier,i*gridPixelMultiplier+gridPixelMultiplier/4),e.restore()}function drawStyledStation_WMATA(e,t,i,a,o,l,r){l||(l="#000000"),r||(r="#ffffff"),o&&(drawCircleStation(e,t,i,a,o,1.2,0,l,0,!0),drawCircleStation(e,t,i,a,o,.9,0,r,0,!0)),drawCircleStation(e,t,i,a,o,.6,0,l,0,!0),drawCircleStation(e,t,i,a,o,.3,0,r,0,!0)}function drawCircleStation(e,t,i,a,o,l,r,n,s,c){o&&!n&&(n="#"+getActiveLine(t,i,a)),!s&&o&&mapLineWidth>=.5&&(s="#ffffff",r=gridPixelMultiplier/2),e.fillStyle=n||"#ffffff",e.beginPath(),e.arc(t*gridPixelMultiplier,i*gridPixelMultiplier,gridPixelMultiplier*l,0,2*Math.PI,!0),e.closePath(),c||(e.lineWidth=r,e.strokeStyle=s||"#"+getActiveLine(t,i,a),e.stroke()),e.fill()}function drawStyledStation_rectangles(e,t,i,a,o,l,r,n,s){var c=getActiveLine(t,i,a,!0);if(3==mapDataVersion)var p="#"+c[0],d=c[1].split("-")[0];else if(2==mapDataVersion)p="#"+c,d=mapLineWidth;else if(1==mapDataVersion)p="#"+c,d=1;var v=getLineDirection(t,i,a).direction,g=[],m=!1,h=getConnectedStations(t,i,a),u=getStation(t,i,a),f=gridPixelMultiplier,y=gridPixelMultiplier;if(d>=.5&&"singleton"!=v||"singleton"==v&&("rect-round"==mapStationStyle||u&&"rect-round"==u.style)?(e.strokeStyle="#000000",e.fillStyle="#ffffff"):(e.strokeStyle=p,e.fillStyle=p),!0===h)f=gridPixelMultiplier/2;else if(u&&h&&"singleton"!=h&&"conflicting"!=h)dx=h.x1-h.x0,dy=h.y1-h.y0,f=(Math.abs(dx)+1)*gridPixelMultiplier,y=(Math.abs(dy)+1)*gridPixelMultiplier,m=!0,dx>0&&0==dy?v="horizontal":0==dx&&dy>0?v="vertical":dx>0&&dy>0?(v="diagonal-ne",f=gridPixelMultiplier):dx>0&&dy<0&&(v="diagonal-ne",y=gridPixelMultiplier);else{if(!h&&!s)return;"singleton"==h&&("singleton"==v?(e.strokeStyle="#000000",d<.5&&("rect-round"==mapStationStyle||u&&"rect-round"==u.style)&&(e.fillStyle=p)):o||(f=gridPixelMultiplier/2))}function b(a,o){if(e.save(),e.translate(t*gridPixelMultiplier,i*gridPixelMultiplier),e.rotate(o),m&&f>y){var l=f/gridPixelMultiplier;f+=l<4?l*(gridPixelMultiplier/4):l*(gridPixelMultiplier/3)}else if(m&&y>f){l=y/gridPixelMultiplier;y+=l<4?l*(gridPixelMultiplier/4):l*(gridPixelMultiplier/3)}MMMDEBUG&&t==$("#station-coordinates-x").val()&&i==$("#station-coordinates-y").val()&&console.log(`stationSpan: ${l}, w1: ${f} h1: ${y}`),n?primitiveRoundRect(e,...a,f,y,n):(e.strokeRect(...a,f,y),e.fillRect(...a,f,y)),e.restore()}if(m&&!s&&(e.strokeStyle="#000000",e.fillStyle="#ffffff"),(!m||!u&&s)&&("rect-round"==mapStationStyle||u&&"rect-round"==u.style)&&(n=2),u&&"rect"==u.style&&(n=!1),e.lineWidth=o?gridPixelMultiplier/2:gridPixelMultiplier/4,s&&!1===h&&(f>y&&(f=y),m=!0),"conflicting"==h&&(v="singleton",f>y&&(f=y),m=!1),l&&(e.strokeStyle=l),r&&(e.fillStyle=r),MMMDEBUG&&t==$("#station-coordinates-x").val()&&i==$("#station-coordinates-y").val()&&console.log(`xy: ${t},${i}; wh: ${f},${y} xf: ${o} ld: ${v} ra: ${g} cs: ${h} dac: ${m} sC: ${l} fC: ${r}`),!m&&("circles-thin"==mapStationStyle&&u&&!u.style||u&&"circles-thin"==u.style))return s||(l="#000000",r="#ffffff"),void drawCircleStation(e,t,i,activeMap,o,.5,e.lineWidth,r,l);if("singleton"==v||!u&&s&&("horizontal"==v||"vertical"==v))g=[(t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,f,y];else if("horizontal"==v&&(m||o))g=[(t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,f,y];else if("horizontal"!=v||m)if("vertical"==v&&(m||o))g=[(t-.5)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,f,y];else if("vertical"!=v||m){if("diagonal-ne"==v&&(m||o))return void b([-.5*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/-4);if("diagonal-ne"==v&&!m)return void b([-.25*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/-4);if("diagonal-se"==v&&(m||o))return void b([-.5*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/4);if("diagonal-se"==v&&!m)return void b([-.25*gridPixelMultiplier,-.5*gridPixelMultiplier],Math.PI/4)}else g=[(t-.5)*gridPixelMultiplier,(i-.25)*gridPixelMultiplier,y,f];else g=[(t-.25)*gridPixelMultiplier,(i-.5)*gridPixelMultiplier,f,y];return n?primitiveRoundRect(e,...g,n):(e.strokeRect(...g),e.fillRect(...g)),m}function primitiveRoundRect(e,t,i,a,o,l){a<2*l&&(l=a/2),o<2*l&&(l=o/2),e.beginPath(),e.moveTo(t+l,i),e.arcTo(t+a,i,t+a,i+o,l),e.arcTo(t+a,i+o,t,i+o,l),e.arcTo(t,i+o,t,i,l),e.arcTo(t,i,t+a,i,l),e.stroke(),e.fill(),e.closePath()}function drawIndicator(e,t){var i=document.getElementById("metro-map-stations-canvas"),a=i.getContext("2d",{alpha:!1}),o=i.width/gridCols;if(getActiveLine(e,t,activeMap)){var l=getStation(e,t,activeMap),r=mapStationStyle;temporaryStation.style?r=temporaryStation.style:l&&l.style&&(r=l.style);var n=temporaryStation.transfer||l&&l.transfer;r&&"wmata"!=r&&"circles-lg"!=r?"circles-md"==r?drawCircleStation(a,e,t,activeMap,n,.3,o/2,"#00ff00","#000000"):"circles-sm"==r?drawCircleStation(a,e,t,activeMap,n,.25,o/4,"#00ff00","#000000"):"rect"==r?drawStyledStation_rectangles(a,e,t,activeMap,n,"#000000","#00ff00",!1,!0):"rect-round"!=r&&"circles-thin"!=r||drawStyledStation_rectangles(a,e,t,activeMap,n,"#000000","#00ff00",20,!0):drawStyledStation_WMATA(a,e,t,activeMap,n,"#000000","#00ff00")}}function drawLabel(e,t,i,a,o){var l=getLabel(t,i,a);if(l||o){o&&!l&&(l={text:"Label",shape:$("#label-shape").val(),"text-color":"#333333"});var r,n,s=e.measureText(l.text).width;r=s0?Math.floor(e/gridPixelMultiplier)*gridPixelMultiplier+gridPixelMultiplier:Math.floor(e/gridPixelMultiplier)*gridPixelMultiplier}function rgb2hex(e){if(/^#[0-9A-F]{6}$/i.test(e))return e;function t(e){return("0"+parseInt(e).toString(16)).slice(-2)}return"#"+t((e=e.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/))[1])+t(e[2])+t(e[3])}function saveMapHistory(e){mapHistory.length>MAX_UNDO_HISTORY&&mapHistory.shift(),JSON.stringify(e)!=window.localStorage.getItem("metroMap")&&(mapHistory.push(JSON.stringify(e)),$("span#undo-buffer-count").text("("+mapHistory.length+")"),$("#tool-undo").prop("disabled",!1)),debugUndoRedo()}function autoSave(e){"object"==typeof e&&(saveMapHistory(activeMap=e),e=JSON.stringify(e)),window.localStorage.setItem("metroMap",e),mapRedoHistory=[],$("#tool-redo").prop("disabled",!0),$("span#redo-buffer-count").text(""),menuIsCollapsed||($("#autosave-indicator").text("Saving locally ..."),$("#title").hide(),setTimeout((function(){$("#autosave-indicator").text(""),$("#title").show()}),1500))}function loadMapFromUndoRedo(e){e&&(window.localStorage.setItem("metroMap",e),$(".rail-line").remove(),loadMapFromObject(e=JSON.parse(e)),setMapSize(e,!0),drawCanvas(e),e.global&&e.global.style?(mapLineWidth=e.global.style.mapLineWidth||mapLineWidth||1,mapStationStyle=e.global.style.mapStationStyle||mapStationStyle||"wmata"):(mapLineWidth=1,mapStationStyle="wmata"),resetResizeButtons(gridCols),resetRailLineTooltips())}function undo(){var e=!1;if(mapHistory.length>1){var t=mapHistory.pop();e=mapHistory[mapHistory.length-1],$("span#undo-buffer-count").text("("+mapHistory.length+")")}else 1==mapHistory.length&&(e=mapHistory[0],$("span#undo-buffer-count").text(""));mapRedoHistory.length>MAX_UNDO_HISTORY&&mapRedoHistory.shift(),(t||e)!=mapRedoHistory[mapRedoHistory.length-1]&&(mapRedoHistory.push(t||e),$("#tool-redo").prop("disabled",!1),$("span#redo-buffer-count").text("("+mapRedoHistory.length+")"),debugUndoRedo(),loadMapFromUndoRedo(e),$(".tooltip").hide())}function redo(){var e=!1;mapRedoHistory.length>=1&&(e=mapRedoHistory.pop(),mapHistory.push(e),$("span#undo-buffer-count").text("("+mapHistory.length+")"),e?(0==mapRedoHistory.length?$("span#redo-buffer-count").text(""):$("span#redo-buffer-count").text("("+mapRedoHistory.length+")"),loadMapFromUndoRedo(e),$(".tooltip").hide()):$("span#redo-buffer-count").text(""))}function debugUndoRedo(){if(MMMDEBUG&&MMMDEBUG_UNDO){$("#announcement").html("");for(var e=0;e"+t+"

")}}}function getURLParameter(e){return decodeURIComponent((new RegExp("[?|&]"+e+"=([^&;]+?)(&|#|;|$)").exec(location.search)||[null,""])[1].replace(/\+/g,"%20"))||null}function autoLoad(){if(gridStep=parseInt(window.localStorage.getItem("metroMapGridStep")||gridStep)||!1,"undefined"!=typeof savedMapData)activeMap=savedMapData;else{if(!window.localStorage.getItem("metroMap"))return $.get("/load/2LVHmJ3r").done((function(e){activeMap=e,"[ERROR]"==e.replace(/\s/g,"").slice(0,7)?(drawGrid(),bindRailLineEvents(),drawCanvas()):(setMapStyle(activeMap=JSON.parse(activeMap)),mapDataVersion=activeMap&&activeMap.global&&activeMap.global.data_version?activeMap.global.data_version:1,compatibilityModeIndicator(),mapSize=setMapSize(activeMap,mapDataVersion>1),loadMapFromObject(activeMap),mapHistory.push(JSON.stringify(activeMap)),setTimeout((function(){$("#tool-resize-"+gridRows).text("Initial size ("+gridRows+"x"+gridCols+")")}),1e3))})).fail((function(e){drawGrid(),bindRailLineEvents(),drawCanvas()})).always((function(){upgradeMapDataVersion(),setMapStyle(activeMap)})),void("undefined"!=typeof autoLoadError&&(autoLoadError+="Loading the default map.",$("#announcement").append('

'+autoLoadError+"

"),setTimeout((function(){$("#autoLoadError").remove()}),3e3)));activeMap=JSON.parse(window.localStorage.getItem("metroMap")),"undefined"!=typeof autoLoadError&&(autoLoadError+="Loading your last-edited map.")}setMapStyle(activeMap),mapDataVersion=activeMap&&activeMap.global&&activeMap.global.data_version?activeMap.global.data_version:1;var e=parseInt(getURLParameter("mapDataVersion"));if(MMMDEBUG&&e>=mapDataVersion)upgradeMapDataVersion(e);else try{upgradeMapDataVersion()}catch(e){console.warn("Error when trying to upgradeMapDataVersion(): "+e)}compatibilityModeIndicator(),mapSize=setMapSize(activeMap,mapDataVersion>1),loadMapFromObject(activeMap),mapHistory.push(JSON.stringify(activeMap)),"undefined"!=typeof autoLoadError&&($("#announcement").append('

'+autoLoadError+"

"),setTimeout((function(){$("#autoLoadError").remove()}),3e3)),setTimeout((function(){$("#tool-resize-"+gridRows).text("Initial size ("+gridRows+"x"+gridCols+")")}),1e3);var t=window.sessionStorage.getItem("zoomLevel");t&&resizeCanvas(t)}function getMapSize(e){var t=0;if("object"!=typeof e&&(e=JSON.parse(e)),3==mapDataVersion)for(var i in e.points_by_color)for(var a in e.points_by_color[i]){for(var o in thisColorHighestValue=Math.max(...Object.keys(e.points_by_color[i][a]).map(Number).filter(Number.isInteger).filter((function(t){return Object.keys(e.points_by_color[i][a][t]).length>0}))),e.points_by_color[i][a]){if(thisColorHighestValue>=ALLOWED_SIZES[ALLOWED_SIZES.length-2]){t=thisColorHighestValue;break}y=Math.max(...Object.keys(e.points_by_color[i][a][o]).map(Number).filter(Number.isInteger)),y>thisColorHighestValue&&(thisColorHighestValue=y)}thisColorHighestValue>t&&(t=thisColorHighestValue)}else if(2==mapDataVersion)for(var i in e.points_by_color){for(var o in thisColorHighestValue=Math.max(...Object.keys(e.points_by_color[i].xys).map(Number).filter(Number.isInteger).filter((function(t){return Object.keys(e.points_by_color[i].xys[t]).length>0}))),e.points_by_color[i].xys){if(thisColorHighestValue>=ALLOWED_SIZES[ALLOWED_SIZES.length-2]){t=thisColorHighestValue;break}y=Math.max(...Object.keys(e.points_by_color[i].xys[o]).map(Number).filter(Number.isInteger)),y>thisColorHighestValue&&(thisColorHighestValue=y)}thisColorHighestValue>t&&(t=thisColorHighestValue)}else if(1==mapDataVersion)for(var o in t=Math.max(...Object.keys(e).map(Number).filter(Number.isInteger).filter((function(t){return Object.keys(e[t]).length>0}))),e){if(t>=ALLOWED_SIZES[ALLOWED_SIZES.length-2])break;y=Math.max(...Object.keys(e[o]).map(Number).filter(Number.isInteger).filter((function(t){return Object.keys(e[o][t]).length>0}))),y>t&&(t=y)}return t}function setMapSize(e,t){var i=gridRows;if(t)gridRows=e.global.map_size,gridCols=e.global.map_size;else for(allowedSize of(highestValue=getMapSize(e),ALLOWED_SIZES))if(highestValue0?$("#tool-line-options button.original-rail-line").remove():e.global.lines={bd1038:{displayName:"Red Line"},df8600:{displayName:"Orange Line"},f0ce15:{displayName:"Yellow Line"},"00b251":{displayName:"Green Line"},"0896d7":{displayName:"Blue Line"},"662c90":{displayName:"Purple Line"},a2a2a2:{displayName:"Silver Line"},"000000":{displayName:"Logo"},"79bde9":{displayName:"Rivers"},cfe4a7:{displayName:"Parks"}};var i=1;for(var a in e.global.lines)e.global.lines.hasOwnProperty(a)&&null===document.getElementById("rail-line-"+a)&&(keyboardShortcut=i<11?' data-toggle="tooltip" title="Keyboard shortcut: '+numberKeys[i-1].replace("Digit","")+'"':i<21?' data-toggle="tooltip" title="Keyboard shortcut: Shift + '+numberKeys[i-1].replace("Digit","")+'"':i<31?' data-toggle="tooltip" title="Keyboard shortcut: Alt + '+numberKeys[i-1].replace("Digit","")+'"':"",$("#line-color-options fieldset").append('"),i++);$((function(){$('[data-toggle="tooltip"]').tooltip({container:"body"}),bindRailLineEvents(),resetRailLineTooltips(),$(".visible-xs").is(":visible")&&($("#canvas-container").removeClass("hidden-xs"),$("#tool-export-canvas").click(),$("#try-on-mobile").attr("disabled",!1))}))}}function updateMapObject(e,t,i,a){if(activeMap)var o=activeMap;else o=JSON.parse(window.localStorage.getItem("metroMap"));if(3==mapDataVersion&&"line"==activeTool?(o.points_by_color[a]||(o.points_by_color[a]={}),o.points_by_color[a][activeLineWidthStyle]||(o.points_by_color[a][activeLineWidthStyle]={})):2==mapDataVersion&&"line"==activeTool&&(o.points_by_color[a]&&o.points_by_color[a].xys||(o.points_by_color[a]={xys:{}})),"eraser"==activeTool){if(3==mapDataVersion){for(var l in a||(a=getActiveLine(e,t,o)),o.points_by_color[a])o.points_by_color&&o.points_by_color[a]&&o.points_by_color[a][l]&&o.points_by_color[a][l][e]&&o.points_by_color[a][l][e][t]&&delete o.points_by_color[a][l][e][t];o.stations&&o.stations[e]&&o.stations[e][t]&&delete o.stations[e][t],o.labels&&o.labels[e]&&o.labels[e][t]&&delete o.labels[e][t]}else 2==mapDataVersion?(a||(a=getActiveLine(e,t,o)),o.points_by_color&&o.points_by_color[a]&&o.points_by_color[a].xys&&o.points_by_color[a].xys[e]&&o.points_by_color[a].xys[e][t]&&delete o.points_by_color[a].xys[e][t],o.stations&&o.stations[e]&&o.stations[e][t]&&delete o.stations[e][t],o.labels&&o.labels[e]&&o.labels[e][t]&&delete o.labels[e][t]):1==mapDataVersion&&o[e]&&o[e][t]&&delete o[e][t];return o}if(1==mapDataVersion&&(o.hasOwnProperty(e)?o[e].hasOwnProperty(t)||(o[e][t]={}):(o[e]={},o[e][t]={})),3==mapDataVersion&&"line"==activeTool)for(var r in o.points_by_color[a][activeLineWidthStyle][e]||(o.points_by_color[a][activeLineWidthStyle][e]={}),o.points_by_color[a][activeLineWidthStyle][e][t]=1,o.points_by_color)for(var l in o.points_by_color[r])r==a&&l==activeLineWidthStyle||o.points_by_color[r][l]&&o.points_by_color[r][l][e]&&o.points_by_color[r][l][e][t]&&delete o.points_by_color[r][l][e][t];else if(2==mapDataVersion&&"line"==activeTool)for(var r in o.points_by_color[a].xys[e]||(o.points_by_color[a].xys[e]={}),o.points_by_color[a].xys[e][t]=1,o.points_by_color)r!=a&&o.points_by_color[r].xys&&o.points_by_color[r].xys[e]&&o.points_by_color[r].xys[e][t]&&delete o.points_by_color[r].xys[e][t];else 1==mapDataVersion&&"line"==activeTool?o[e][t].line=a:"station"==activeTool?2==mapDataVersion||3==mapDataVersion?(o.stations||(o.stations={}),o.stations[e]||(o.stations[e]={}),o.stations[e][t]||(o.stations[e][t]={}),o.stations[e][t][i]=a):o[e][t].station[i]=a:"label"==activeTool&&(2!=mapDataVersion&&3!=mapDataVersion||(o.labels||(o.labels={}),o.labels[e]||(o.labels[e]={}),o.labels[e][t]||(o.labels[e][t]={}),i?o.labels[e][t][i]=a:o.labels[e][t]=a));return o}function moveMap(e){if("station"!=activeTool||!$("#tool-station-options").is(":visible")){var t=0,i=0;if("left"==e)t=-1;else if("right"==e)t=1;else if("down"==e)i=1;else if("up"==e)i=-1;if(3==mapDataVersion){var a={},o={};for(var l in activeMap.points_by_color)for(var r in a[l]={},activeMap.points_by_color[l])for(var n in a[l][r]={},activeMap.points_by_color[l][r])for(var s in activeMap.points_by_color[l][r][n])if(n=parseInt(n),s=parseInt(s),Number.isInteger(n)&&Number.isInteger(s)){if(a[l][r][n+t]||(a[l][r][n+t]={}),0==n&&"left"==e)return;if(n==gridCols-1&&"right"==e)return;if(0==s&&"up"==e)return;if(s==gridRows-1&&"down"==e)return;0<=n&&n=gridCols&&(n=gridCols-1),s<0?s=0:s>=gridRows&&(s=gridRows-1),[n,s]}function ffSpan(e,t,i){return i?!(!e||!t||e[0]==t[0]&&e[1]==t[1])||(!(!e||t)||!(e||!t)):void 0===e&&void 0===t||!(!e||!t||e[0]!=t[0]||e[1]!=t[1])}function floodFill(e,t,i,a,o){if(!(mapDataVersion>=3&&i&&i[0]==a&&i[1]==activeLineWidthStyle)&&!(i==a&&mapDataVersion<3)&&e&&t){var l=!1,r=spanBelow=0,n=[e,t],s={};if(o){var c=document.getElementById("hover-canvas");c.getContext("2d").clearRect(0,0,c.width,c.height)}if(mapDataVersion<=2)for(;n.length>0;){for(t=n.pop(),l=e=n.pop();l>=0&&getActiveLine(l,t,activeMap,mapDataVersion>=3)==i;)l--;for(l++,r=spanBelow=0;l=3)==i;){if(o){if(s&&s[l]&&s[l][t])break;s.hasOwnProperty(l)||(s[l]={}),drawHoverIndicator(l,t,a,.5),s[l][t]=!0}else updateMapObject(l,t,"line",a);!r&&t>0&&getActiveLine(l,t-1,activeMap,mapDataVersion>=3)==i?(n.push(l,t-1),r=1):r&&t>0&&getActiveLine(l,t-1,activeMap,mapDataVersion>=3)!=i&&(r=0),!spanBelow&&t=3)==i?(n.push(l,t+1),spanBelow=1):spanBelow&&t=3)!=i&&(spanBelow=0),l++}}else for(;n.length>0;){for(t=n.pop(),l=e=n.pop();l>=0&&ffSpan(getActiveLine(l,t,activeMap,mapDataVersion>=3),i);)l--;for(l++,r=spanBelow=0;l=3),i);){if(o){if(s&&s[l]&&s[l][t])break;s.hasOwnProperty(l)||(s[l]={}),drawHoverIndicator(l,t,a,.5),s[l][t]=!0}else updateMapObject(l,t,"line",a);!r&&t>0&&ffSpan(getActiveLine(l,t-1,activeMap,mapDataVersion>=3),i)?(n.push(l,t-1),r=1):r&&t>0&&ffSpan(getActiveLine(l,t-1,activeMap,mapDataVersion>=3),i,!0)&&(r=0),!spanBelow&&t=3),i)?(n.push(l,t+1),spanBelow=1):spanBelow&&t=3),i,!0)&&(spanBelow=0),l++}}}}function combineCanvases(){drawCanvas(activeMap);var e=document.getElementById("metro-map-canvas"),t=document.getElementById("metro-map-stations-canvas");return e.getContext("2d",{alpha:!1}).drawImage(t,0,0),t.getContext("2d",{alpha:!0}).clearRect(0,0,t.width,t.height),e}function downloadImage(e,t){var i="metromapmaker.png";if(HTMLCanvasElement.prototype.toBlob)pngUrl&&URL.revokeObjectURL(pngUrl),e.toBlob((function(e){pngUrl=URL.createObjectURL(e),$("#metro-map-image-download-link").attr({download:i,href:pngUrl}),t?($("#grid-canvas").hide(),$("#hover-canvas").hide(),$("#ruler-canvas").hide(),$("#metro-map-canvas").hide(),$("#metro-map-stations-canvas").hide(),$("#metro-map-image").attr("src",pngUrl),$("#metro-map-image").show()):(document.getElementById("metro-map-image-download-link").click(),drawCanvas(activeMap))}));else{var a=e.toDataURL();$("#metro-map-image-download-link").attr({download:i,href:a}),t?($("#grid-canvas").hide(),$("#hover-canvas").hide(),$("#ruler-canvas").hide(),$("#metro-map-canvas").hide(),$("#metro-map-stations-canvas").hide(),$("#metro-map-image").attr("src",a),$("#metro-map-image").show()):(document.getElementById("metro-map-image-download-link").click(),drawCanvas(activeMap))}}function resetResizeButtons(e){$(".resize-grid").each((function(){if("Current"==$(this).html().split(" ")[0]){var e=$(this).attr("id").split("-").slice(2),t=e+"x"+e;$(this).text(t)}})),$("#tool-resize-"+e).text("Current size ("+e+"x"+e+")"),isMapStretchable(e)?($("#tool-resize-stretch").show(),$("#tool-resize-stretch").text("Stretch map to "+2*e+"x"+2*e)):$("#tool-resize-stretch").hide()}function throttle(e,t){let i=!0;return function(...a){i&&(i=!1,e.apply(this,a),setTimeout((()=>i=!0),t))}}function resetTooltipOrientation(){tooltipOrientation="",window.innerWidth<=768?tooltipOrientation="top":$("#snap-controls-right").is(":hidden")?tooltipOrientation="left":tooltipOrientation="right",$(".has-tooltip").each((function(){$(this).data("bs.tooltip")&&($(this).data("bs.tooltip").options.placement=tooltipOrientation)}))}function resetRailLineTooltips(){for(var e=$(".rail-line"),t="",i=0;i=3),indicatorColor,!0));else{$("#tool-line-icon-pencil").show(),$("#tool-eraser-icon-eraser").show(),$("#tool-line-icon-paint-bucket").hide(),$("#tool-line-caption-fill").hide(),menuIsCollapsed||($("#tool-line-caption-draw").show(),$("#tool-eraser-caption-eraser").show()),$("#tool-eraser-icon-paint-bucket").hide(),$("#tool-eraser-caption-fill").hide(),$("#tool-eraser-options").hide();var e=document.getElementById("hover-canvas");e.getContext("2d").clearRect(0,0,e.width,e.height),("line"==activeTool&&activeToolOption||"eraser"==activeTool)&&(indicatorColor="line"==activeTool&&activeToolOption?activeToolOption:"#ffffff",drawHoverIndicator(hoverX,hoverY,indicatorColor))}}function setURLAfterSave(e){window.location.href.split("=")[0]}function getSurroundingLine(e,t,i,a){return getActiveLine((e=parseInt(e))-1,t=parseInt(t),i)&&getActiveLine(e-1,t,i)==getActiveLine(e+1,t,i)?getActiveLine(e-1,t,i,a):getActiveLine(e,t-1,i)&&getActiveLine(e,t-1,i)==getActiveLine(e,t+1,i)?getActiveLine(e,t-1,i,a):getActiveLine(e-1,t-1,i)&&getActiveLine(e-1,t-1,i)==getActiveLine(e+1,t+1,i)?getActiveLine(e-1,t-1,i,a):getActiveLine(e-1,t+1,i)&&getActiveLine(e-1,t+1,i)==getActiveLine(e+1,t-1,i)?getActiveLine(e-1,t+1,i,a):void 0}function setAllStationOrientations(e,t){if(t=parseInt(t),-1!=ALLOWED_ORIENTATIONS.indexOf(t))if(2==mapDataVersion||3==mapDataVersion)for(var i in e.stations)for(var a in e.stations[i])i=parseInt(i),a=parseInt(a),e.stations[i][a].orientation=t;else if(1==mapDataVersion)for(var i in e)for(var a in e[i])i=parseInt(i),a=parseInt(a),Number.isInteger(i)&&Number.isInteger(a)&&-1!=Object.keys(e[i][a]).indexOf("station")&&(e[i][a].station.orientation=t)}function resetAllStationStyles(e){if(2==mapDataVersion||3==mapDataVersion)for(var t in e.stations)for(var i in e.stations[t])t=parseInt(t),i=parseInt(i),e.stations[t][i].style&&delete e.stations[t][i].style;else if(1==mapDataVersion)for(var t in e)for(var i in e[t])t=parseInt(t),i=parseInt(i),Number.isInteger(t)&&Number.isInteger(i)&&-1!=Object.keys(e[t][i]).indexOf("station")&&-1!=Object.keys(e[t][i].station).indexOf("style")&&delete e[t][i].station.style}function combineLineColorWidthStyle(e){return"object"==typeof e?e.join("-"):e}function getLineDirection(e,t,i){return e=parseInt(e),t=parseInt(t),origin=combineLineColorWidthStyle(getActiveLine(e,t,i,mapDataVersion>=3)),NW=combineLineColorWidthStyle(getActiveLine(e-1,t-1,i,mapDataVersion>=3)),NE=combineLineColorWidthStyle(getActiveLine(e+1,t-1,i,mapDataVersion>=3)),SW=combineLineColorWidthStyle(getActiveLine(e-1,t+1,i,mapDataVersion>=3)),SE=combineLineColorWidthStyle(getActiveLine(e+1,t+1,i,mapDataVersion>=3)),N=combineLineColorWidthStyle(getActiveLine(e,t-1,i,mapDataVersion>=3)),E=combineLineColorWidthStyle(getActiveLine(e+1,t,i,mapDataVersion>=3)),S=combineLineColorWidthStyle(getActiveLine(e,t+1,i,mapDataVersion>=3)),W=combineLineColorWidthStyle(getActiveLine(e-1,t,i,mapDataVersion>=3)),info={direction:!1,endcap:!1},origin?(origin==W&&W==E?info.direction="horizontal":origin==N&&N==S?info.direction="vertical":origin==NW&&NW==SE?info.direction="diagonal-se":origin==SW&&SW==NE?info.direction="diagonal-ne":origin==W||origin==E?(info.direction="horizontal",info.endcap=!0):origin==N||origin==S?(info.direction="vertical",info.endcap=!0):origin==NW||origin==SE?(info.direction="diagonal-se",info.endcap=!0):origin==SW||origin==NE?(info.direction="diagonal-ne",info.endcap=!0):info.direction="singleton",info):info}function getConnectedStations(e,t,i){if(getStation(e=parseInt(e),t=parseInt(t),i)){var a=getStation(e-1,t-1,i),o=getStation(e+1,t-1,i),l=getStation(e-1,t+1,i),r=getStation(e+1,t+1,i),n=getStation(e,t-1,i),s=getStation(e+1,t,i),c=getStation(e,t+1,i),p=getStation(e-1,t,i);if(!(a||o||l||r||n||s||c||p))return"singleton";var d={highest:{E:0,S:0,SE:0,NE:0},E:{},S:{},N:{},W:{},NE:{},SE:{},SW:{},NW:{}};if(s&&f(s)){var v=e,g=t;do{v+=1}while(f(getStation(v,g,i)));d.E={x1:v-1,y1:g},d.highest.E=v-e-1}if(c&&f(c)){v=e,g=t;do{g+=1}while(f(getStation(v,g,i)));d.S={x1:v,y1:g-1},d.highest.S=g-t-1}if(o&&f(o)){v=e,g=t;do{v+=1,g-=1}while(f(getStation(v,g,i)));d.NE={x1:v-1,y1:g+1},d.highest.NE=v-e-1}if(r&&f(r)){v=e,g=t;do{v+=1,g+=1}while(f(getStation(v,g,i)));d.SE={x1:v-1,y1:g-1},d.highest.SE=v-e-1}if(p&&f(p)){v=e,g=t;do{v-=1}while(f(getStation(v,g,i)));d.W={x1:v+1,y1:g},d.E.internal=!0,d.highest.E+=Math.abs(v-e)-1}if(n&&f(n)){v=e,g=t;do{g-=1}while(f(getStation(v,g,i)));d.N={x1:v,y1:g+1},d.S.internal=!0,d.highest.S+=Math.abs(g-t)-1}if(a&&f(a)){v=e,g=t;do{v-=1,g-=1}while(f(getStation(v,g,i)));d.NW={x1:v+1,y1:g+1},d.SE.internal=!0,d.highest.SE+=Math.abs(v-e)-1}if(l&&f(l)){v=e,g=t;do{v-=1,g+=1}while(f(getStation(v,g,i)));d.SW={x1:v+1,y1:g-1},d.NE.internal=!0,d.highest.NE+=Math.abs(v-e)-1}var m=Object.values(d.highest).filter((e=>e>0)),h=Math.max(...Object.values(m));if(0==m.length)return"singleton";if(m.indexOf(h)!=m.lastIndexOf(h))return"conflicting";var u=Object.keys(d.highest).filter((e=>!0!==Number.isNaN(e))).sort((function(e,t){return d.highest[e]-d.highest[t]})).reverse();return!u||!u[0]||!d[u[0]].internal&&{x0:e,y0:t,x1:d[u[0]].x1,y1:d[u[0]].y1}}function f(e){return!!e&&("rect"==e.style||"rect-round"==e.style||"circles-thin"==e.style||!(e.style||"rect"!=mapStationStyle&&"rect-round"!=mapStationStyle&&"circles-thin"!=mapStationStyle))}}function stretchMap(e){e||(e=activeMap);var t={};if(t.global=Object.assign({},activeMap.global),3==mapDataVersion){for(var i in t.points_by_color={},e.points_by_color)for(var a in t.points_by_color[i]={},e.points_by_color[i])for(var o in t.points_by_color[i][a]={},e.points_by_color[i][a])for(var l in e.points_by_color[i][a][o])o=parseInt(o),l=parseInt(l),e.points_by_color[i][a][o][l]&&(2*o>MAX_MAP_SIZE-1||2*l>MAX_MAP_SIZE-1||(t.points_by_color[i][a].hasOwnProperty(2*o)||(t.points_by_color[i][a][2*o]={}),t.points_by_color[i][a][2*o][2*l]=e.points_by_color[i][a][o][l]));for(var o in setMapSize(t),resetResizeButtons(gridCols),t.stations={},e.stations)for(var l in e.stations[o])o=parseInt(o),l=parseInt(l),2*o>=gridRows||2*l>=gridCols||(t.stations.hasOwnProperty(2*o)||(t.stations[2*o]={}),t.stations[2*o][2*l]=Object.assign({},e.stations[o][l]));for(o=1;oMAX_MAP_SIZE-1||2*l>MAX_MAP_SIZE-1||(t.points_by_color[i].xys.hasOwnProperty(2*o)||(t.points_by_color[i].xys[2*o]={}),t.points_by_color[i].xys[2*o][2*l]=e.points_by_color[i].xys[o][l]));for(var o in setMapSize(t),resetResizeButtons(gridCols),t.stations={},e.stations)for(var l in e.stations[o])o=parseInt(o),l=parseInt(l),2*o>=gridRows||2*l>=gridCols||(t.stations.hasOwnProperty(2*o)||(t.stations[2*o]={}),t.stations[2*o][2*l]=Object.assign({},e.stations[o][l]));for(o=1;o-1?$("#snap-controls-left").hide():$("#snap-controls-right").hide(),$("#toolbox button span.button-label").show(),$("#title, #remix, #credits, #rail-line-new, #rail-line-change, #rail-line-delete, #straight-line-assist-options, #flood-fill-options, #tool-move-all, #tool-resize-all").show(),mapDataVersion>=2&&$("#tool-map-style").show(),mapDataVersion>=3&&$("#line-style-options").show(),$("#tool-move-all, #tool-resize-all").removeClass("width-100"),$("#tool-flood-fill").prop("checked")?($("#tool-line-caption-draw").hide(),$("#tool-eraser-caption-eraser").hide()):($("#tool-line-caption-fill").hide(),$("#tool-eraser-caption-fill").hide()),1==$("#hide-save-share-url").length&&$("#hide-save-share-url").show(),"Name this map"==$("#name-this-map").text()&&$("#name-map, #name-this-map").show(),$("#controls-collapse-menu").show(),$("#controls-expand-menu").hide()}}function colorInUse(e){if(!activeMap||!activeMap.points_by_color||!activeMap.points_by_color[e])return!1;if(3==mapDataVersion){for(var t in activeMap.points_by_color[e])for(var i in activeMap.points_by_color[e][t])for(var a in activeMap.points_by_color[e][t][i])if(activeMap.points_by_color[e][t][i][a])return!0}else if(2==mapDataVersion){if(!activeMap.points_by_color[e].xys)return;for(var i in activeMap.points_by_color[e].xys)for(var a in activeMap.points_by_color[e].xys[i])if(activeMap.points_by_color[e].xys[i][a])return!0}}function cycleLineWidth(){var e=ALLOWED_LINE_WIDTHS.indexOf(100*activeLineWidth);-1==e||e==ALLOWED_LINE_WIDTHS.length-1?e=0:e+=1;var t=$('button[data-linewidth="'+ALLOWED_LINE_WIDTHS[e]+'"]');t.length||(t=$('button[data-linewidth="'+ALLOWED_LINE_WIDTHS[e]+'.0"]')),t.trigger("click")}function cycleLineStyle(){var e=ALLOWED_LINE_STYLES.indexOf(activeLineStyle);-1==e||e==ALLOWED_LINE_STYLES.length-1?e=0:e+=1,$('button[data-linestyle="'+ALLOWED_LINE_STYLES[e]+'"]').trigger("click")}function setLineStyle(e,t){var i;"solid"==e?(i=[],t.lineCap="round"):"dashed"==e?(i=[gridPixelMultiplier,1.5*gridPixelMultiplier],t.lineCap="square"):"dotted_dense"==e?(t.lineCap="butt",i=[gridPixelMultiplier/2,gridPixelMultiplier/4]):"dotted"==e?(t.lineCap="butt",i=[gridPixelMultiplier/2,gridPixelMultiplier/2]):"dense_thick"==e?(i=[2,2],t.lineCap="butt"):"dense_thin"==e?(i=[1,1],t.lineCap="butt"):(i=[],t.lineCap="round"),t.setLineDash(i)}function unfreezeMapControls(){window.innerWidth>768&&$("#tool-line").prop("disabled")&&$("#tool-export-canvas").click(),resetTooltipOrientation()}function drawRuler(e,t,i){var a=document.getElementById("ruler-canvas"),o=a.getContext("2d");o.globalAlpha=.33,o.fillStyle="#2ECC71",o.strokeStyle="#2ECC71";var l=a.width/gridCols;if(0==rulerOrigin.length)o.fillRect((e-.5)*l,(t-.5)*l,l,l);else if(rulerOrigin.length>0){o.clearRect(0,0,a.width,a.height),o.fillRect((rulerOrigin[0]-.5)*l,(rulerOrigin[1]-.5)*l,l,l),o.fillRect((e-.5)*l,(t-.5)*l,l,l),o.beginPath(),o.lineWidth=l,o.lineCap="round",o.moveTo(rulerOrigin[0]*l,rulerOrigin[1]*l),o.lineTo(e*l,t*l),o.stroke(),o.closePath(),o.textAlign="start",o.font="700 "+l+"px sans-serif",o.globalAlpha=.67,o.fillStyle="#000000";var r="",n=Math.abs(rulerOrigin[0]-e),s=Math.abs(rulerOrigin[1]-t);!n&&s||n&&!s?r+=n+s:n&&s&&(r+=n+", "+s),o.fillText(r,(e+1)*l,(t+1)*l)}i&&(rulerOrigin=[e,t])}function cycleGridStep(){var e=[3,5,7,!1],t=e.indexOf(gridStep);(gridStep=-1==t||t==e.length-1?e[0]:e[t+1])?showGrid():hideGrid(),window.localStorage.setItem("metroMapGridStep",gridStep),drawGrid()}function restyleAllLines(e,t,i){var a={points_by_color:{}};for(var o in a.stations=Object.assign({},activeMap.stations),a.global=Object.assign({},activeMap.global),activeMap.points_by_color)for(var l in activeMap.points_by_color[o]){if(mapDataVersion>=3)var r=l.split("-")[0],n=l.split("-")[1];var s=(e||r)+"-"+(t||n);a.points_by_color[o]||(a.points_by_color[o]={}),a.points_by_color[o][s]||(a.points_by_color[o][s]={}),a.points_by_color[o][s]=Object.assign(a.points_by_color[o][s],activeMap.points_by_color[o][l])}2==mapDataVersion&&(mapDataVersion=3,a.global.data_version=3,compatibilityModeIndicator()),activeMap=a,i||(autoSave(activeMap),drawCanvas(activeMap))}function upgradeMapDataVersion(e){if(1==mapDataVersion){if(e&&1==e)return $("#line-style-options").hide(),$("#tool-map-style, #tool-map-style-options").hide(),void $('#station-style, label[for="station-style"]').hide();var t={points_by_color:{},stations:{}};t.global=Object.assign({},activeMap.global);var i=getMapSize(activeMap)||0;for(allowedSize of ALLOWED_SIZES)if(i=0?i=numberKeys.indexOf(e.code)+10:!e.metaKey&&!e.ctrlKey&&e.altKey&&numberKeys.indexOf(e.code)>=0?i=numberKeys.indexOf(e.code)+20:!e.metaKey&&!e.ctrlKey&&numberKeys.indexOf(e.code)>=0&&(i=numberKeys.indexOf(e.code)):(e.preventDefault(),moveMap("right")):(e.preventDefault(),moveMap("left")):mapDataVersion>=3&&cycleLineStyle():mapDataVersion>=3&&cycleLineWidth():$("#tool-ruler").trigger("click"):!menuIsCollapsed&&mapDataVersion>1&&$("#tool-map-style").trigger("click"):menuIsCollapsed?$("#controls-expand-menu").trigger("click"):$("#controls-collapse-menu").trigger("click"),!1!==i&&t[i]&&t[i].click()}})),activeTool="look",$("#toolbox button:not(.rail-line)").on("click",(function(){$(".active").removeClass("active"),$(this).addClass("active"),"line"==activeTool?$("#tool-line").addClass("active"):"eraser"==activeTool?$("#tool-eraser").addClass("active"):"station"==activeTool&&$("#tool-station").addClass("active"),rulerOn||"tool-ruler"!=$(this).attr("id")||$(this).removeClass("active")})),$("#toolbox button.rail-line").on("click",(function(){$(".active").removeClass("active"),$("#tool-line").addClass("active")})),$("#tool-line").on("click",(function(){$(".active").removeClass("active"),$("#tool-line").addClass("active"),$(this).hasClass("draw-rail-line")&&activeToolOption?(activeTool="line",$(this).css({"background-color":activeToolOption})):"eraser"==activeTool&&activeToolOption?activeTool="line":"eraser"==activeTool&&(activeTool="look"),$("#tool-line-options").is(":visible")?($("#tool-line-options").hide(),$("#tool-new-line-options").hide(),$("#tool-station").hasClass("width-100")||$(this).removeClass("width-100"),$("#rail-line-new span").text("Add New Line"),$("#tool-new-line-options").hide()):($("#tool-line-options").show(),$(this).addClass("width-100")),$(".tooltip").hide()})),$("#tool-flood-fill").change((function(){setFloodFillUI()})),$("#rail-line-delete").click((function(){var e=$(".rail-line"),t=[],i=Object.assign({},activeMap);if("line"==activeTool&&(activeTool="look",$("#tool-line").attr("style",""),$("#tool-line").removeClass("active")),2==mapDataVersion||3==mapDataVersion)for(var a of e)colorInUse(a=a.id.slice(10,16))||(t.push($("#rail-line-"+a)),delete activeMap.global.lines[a]);else if(1==mapDataVersion){delete i.global,i=JSON.stringify(i);for(var o=0;o0){autoSave(activeMap);for(var l=0;l=2&&(activeMap.global.map_size=parseInt(size)),setMapSize(activeMap,!0),activeTool=e,autoSave(activeMap)})),$("#tool-resize-stretch").click((function(){stretchMap(),autoSave(activeMap)})),$("#tool-move-all").on("click",(function(){$("#tool-move-options").is(":visible")?($("#tool-move-options").hide(),$(this).removeClass("active"),$(this).removeClass("width-100")):($("#tool-move-options").show(),$(this).addClass("width-100")),$(".tooltip").hide()})),$("#tool-move-up").click((function(){moveMap("up")})),$("#tool-move-down").click((function(){moveMap("down")})),$("#tool-move-left").click((function(){moveMap("left")})),$("#tool-move-right").click((function(){moveMap("right")})),$("#tool-save-map").click((function(){activeTool="look";var e=JSON.stringify(activeMap);autoSave(e);csrftoken=getCookie("csrftoken"),$("#tool-save-map").prop("disabled","disabled"),$("#tool-save-map span").text("Saving ..."),setTimeout((function(){$("#tool-save-map").prop("disabled",!1)}),1e3),$.post("/save/",{metroMap:e,csrfmiddlewaretoken:csrftoken}).done((function(e){if(e.replace(/\n/g,"").indexOf("No points_by_color")>-1)$("#tool-save-options").html('
You can\'t save an empty map.
'),$("#tool-save-options").show();else if("[ERROR]"==e.replace(/\s/g,"").slice(0,7))$("#tool-save-options").html('
Sorry, there was a problem saving your map: '+e.slice(9)+"
"),console.log("[WARN] Problem was: "+e),$("#tool-save-options").show();else{menuIsCollapsed&&$("#controls-expand-menu").trigger("click");var t=(e=e.split(","))[0].replace(/\s/g,""),i=e[1].replace(/\s/g,""),a='
Map Saved! You can share your map with a friend by using this link: metromapmaker.com/map/'+t+'
You can then share this URL with a friend - and they can remix your map without you losing your original! If you make changes to this map, click Save & Share again to get a new URL.
';i&&(a+='
');var o=window.sessionStorage.getItem("userGivenMapName"),l=window.sessionStorage.getItem("userGivenMapTags");i&&o&&l&&(a+='
Not a map of '+o+"? Click here to rename
"),$("#tool-save-options").html("
Save & Share"+a+"
"),i&&o&&$("#user-given-map-name").val(o),i&&l&&$("#user-given-map-tags").val(l),$("#name-map").submit((function(e){e.preventDefault()})),$("#map-somewhere-else").click((function(){$("#name-map").show(),$("#name-this-map").show(),$(this).parent().hide(),$("#name-this-map").removeClass(),$("#name-this-map").addClass("styling-blueline width-100"),$("#name-this-map").text("Name this map")})),$("#name-this-map").click((function(e){$("#user-given-map-name").val($("#user-given-map-name").val().replaceAll("<","").replaceAll(">","").replaceAll('"',"").replaceAll("\\\\","").replace("&","&").replaceAll("&","&").replaceAll("/","-").replaceAll("'",""));var t=$("#name-map").serializeArray().reduce((function(e,t){return e[t.name]=t.value,e}),{});window.sessionStorage.setItem("userGivenMapName",$("#user-given-map-name").val()),window.sessionStorage.setItem("userGivenMapTags",$("#user-given-map-tags").val()),csrftoken=getCookie("csrftoken"),t.csrfmiddlewaretoken=csrftoken,$.post("/name/",t,(function(){$("#name-map").hide(),$("#name-this-map").text("Thanks!"),setTimeout((function(){$("#name-this-map").hide()}),500)}))})),i&&o&&l&&($("#user-given-map-name").show(),$("#user-given-map-tags").show(),$("#name-this-map").click(),$("#name-this-map").hide()),$("#tool-save-options").show(),$("#hide-save-share-url").click((function(){$("#tool-save-options").hide()}))}})).fail((function(e){if(400==e.status)var t="Sorry, your map could not be saved. Did you flood fill the whole map? Use flood fill with the eraser to erase and try again.";else if(500==e.status)t="Sorry, your map could not be saved right now. This may be a bug, and the admin has been notified.";else if(e.status>=502)t="Sorry, your map could not be saved right now. Metro Map Maker is currently undergoing routine maintenance including bugfixes and upgrades. Please try again in a few minutes.";$("#tool-save-options").html('
'+t+"
"),$("#tool-save-options").show()})).always((function(){setTimeout((function(){$("#tool-save-map span").text("Save & Share")}),350)})),$(".tooltip").hide()})),$("#tool-download-image").click((function(){activeTool="look",downloadImage(combineCanvases()),$(".tooltip").hide()})),$("#tool-export-canvas").click((function(){if(activeTool="look",drawCanvas(activeMap),$("#tool-station-options").hide(),$(".tooltip").hide(),$("#grid-canvas").is(":visible")){$("#grid-canvas").hide(),$("#hover-canvas").hide(),$("#ruler-canvas").hide(),$("#metro-map-canvas").hide(),$("#metro-map-stations-canvas").hide();var e=document.getElementById("metro-map-canvas"),t=document.getElementById("metro-map-stations-canvas");e.getContext("2d",{alpha:!1}).drawImage(t,0,0);var i=e.toDataURL();$("#metro-map-image").attr("src",i),$("#metro-map-image").show(),$("#export-canvas-help").show(),$("button:not(.mobile-browse)").attr("disabled",!0),$(this).attr("disabled",!1),$(this).attr("title","Go back to editing your map").tooltip("fixTitle").tooltip("show")}else $("#grid-canvas").show(),$("#hover-canvas").show(),$("#ruler-canvas").show(),$("#metro-map-canvas").show(),$("#metro-map-stations-canvas").show(),$("#metro-map-image").hide(),$("#export-canvas-help").hide(),$("button").attr("disabled",!1),$(this).attr("title","Download your map to share with friends").tooltip("fixTitle").tooltip("show")})),$("#tool-clear-map").click((function(){gridRows=80,gridCols=80,(activeMap={global:Object.assign({},activeMap.global),points_by_color:{},stations:{}}).global.map_size=80,snapCanvasToGrid(),drawGrid(),lastStrokeStyle=void 0,drawCanvas(activeMap,!1,!0),drawCanvas(activeMap,!0,!0),window.sessionStorage.removeItem("userGivenMapName"),window.sessionStorage.removeItem("userGivenMapTags"),$(".resize-grid").each((function(){var e=$(this).attr("id").split("-").slice(2),t=e+" x "+e;e==ALLOWED_SIZES[0]&&(t+=" (Current size)"),$(this).html(t)})),showGrid(),$(".tooltip").hide()})),$("#rail-line-new").click((function(){$("#tool-new-line-options").is(":visible")?($(this).children("span").text("Add New Line"),$("#tool-new-line-options").hide()):($(this).children("span").text("Hide Add Line options"),$("#tool-new-line-options").show())})),$("#create-new-rail-line").click((function(){$("#new-rail-line-name").val($("#new-rail-line-name").val().replaceAll("<","").replaceAll(">","").replaceAll('"',"").replaceAll("\\\\","").replaceAll("/","-"));var e=[],t=[];for(var i in $(".rail-line").each((function(){e.push($(this).attr("id").slice(10,16)),t.push($(this).text())})),""==$("#new-rail-line-color").val()&&$("#new-rail-line-color").val("#000000"),e.indexOf($("#new-rail-line-color").val().slice(1,7))>=0?$("#tool-new-line-errors").text("This color already exists! Please choose a new color."):t.indexOf($("#new-rail-line-name").val())>=0?$("#tool-new-line-errors").text("This rail line name already exists! Please choose a new name."):0==$("#new-rail-line-name").val().length?$("#tool-new-line-errors").text("This rail line name cannot be blank. Please enter a name."):$("#new-rail-line-name").val().length>100?$("#tool-new-line-errors").text("This rail line name is too long. Please shorten it."):$(".rail-line").length>99?$("#tool-new-line-errors").text("Too many rail lines! Delete your unused ones before creating new ones."):($("#tool-new-line-errors").text(""),$("#line-color-options fieldset").append('"),activeMap.global||(activeMap.global={lines:{}}),activeMap.global.lines||(activeMap.global.lines={}),$(".rail-line").each((function(){"rail-line-new"!=$(this).attr("id")&&(activeMap.global.lines[$(this).attr("id").slice(10,16)]={displayName:$(this).text()})})),autoSave(activeMap)),bindRailLineEvents(),resetRailLineTooltips(),$("#tool-lines-to-change").html(""),activeMap.global.lines)$("#tool-lines-to-change").append('")})),$("#rail-line-change").click((function(){for(var e in $("#tool-change-line-options").is(":visible")?($(this).children("span").html("Edit colors & names"),$("#tool-change-line-options").hide()):($(this).children("span").text("Close Edit Line options"),$("#tool-change-line-options").show()),$("#tool-lines-to-change").html(""),$("#change-line-name").hide(),$("#change-line-color").hide(),$("#tool-change-line-options label").hide(),$("#tool-change-line-options p").text(""),activeMap.global.lines)$("#tool-lines-to-change").append('")})),$("#tool-lines-to-change").on("change",(function(){$("#tool-change-line-options label").show(),"Edit which rail line?"!=$("#tool-lines-to-change option:selected").text()?($("#change-line-name").show(),$("#change-line-color").show(),$("#change-line-name").val($("#tool-lines-to-change option:selected").text()),$("#change-line-color").val("#"+$(this).val())):($("#tool-change-line-options p").text(""),$("#change-line-name").hide(),$("#change-line-color").hide())})),$("#save-rail-line-edits").click((function(){if("Edit which rail line?"!=$("#tool-lines-to-change option:selected").text()){var e=$("#tool-lines-to-change").val(),t=$("#change-line-color").val().slice(1),i=$("#tool-lines-to-change option:selected").text(),a=$("#change-line-name").val().replaceAll("<","").replaceAll(">","").replaceAll('"',"").replaceAll("\\\\","").replaceAll("/","-"),o=[];$(".rail-line").each((function(){o.push($(this).text())})),e!=t&&Object.keys(activeMap.global.lines).indexOf(t)>=0?$("#cant-save-rail-line-edits").text("Can't change "+i+" - it has the same color as "+activeMap.global.lines[t].displayName):o.indexOf(a)>-1&&i!=a?$("#cant-save-rail-line-edits").text("This rail line name already exists! Please choose a new name."):(replaceColors({color:e,name:i},{color:t,name:a}),$("#rail-line-change span").html("Edit colors & names"),$("#cant-save-rail-line-edits").text(""),$("#tool-change-line-options").hide(),"line"==activeTool&&(activeTool="look",$("#tool-line").attr("style",""),$("#tool-line").removeClass("active")))}"line"==activeTool&&rgb2hex(activeToolOption).slice(1,7)==e&&(activeToolOption="#"+t),resetRailLineTooltips()})),$("#tool-map-style").on("click",(function(){$("#tool-map-style-options").toggle(),$("#tool-map-style-options").is(":visible")||$("#tool-map-style").removeClass("active"),$(".tooltip").hide(),.75==mapLineWidth?$("#tool-map-style-line-750").addClass("active-mapstyle"):$("#tool-map-style-line-"+1e3*mapLineWidth).addClass("active-mapstyle"),$("#tool-map-style-station-"+mapStationStyle).addClass("active-mapstyle")})),$(".map-style-line").on("click",(function(){mapLineWidth="tool-map-style-line-750"==$(this).attr("id")?.75:1/parseInt($(this).data("line-width-divisor")),$(".map-style-line.active-mapstyle").removeClass("active-mapstyle"),$(this).addClass("active-mapstyle"),activeMap&&activeMap.global&&activeMap.global.style?activeMap.global.style.mapLineWidth=mapLineWidth:activeMap&&activeMap.global&&(activeMap.global.style={mapLineWidth:mapLineWidth}),mapDataVersion>=3?restyleAllLines(mapLineWidth):drawCanvas(activeMap),autoSave(activeMap)})),$(".map-style-station").on("click",(function(){mapStationStyle=$(this).data("station-style"),$(".map-style-station.active-mapstyle").removeClass("active-mapstyle"),$(this).addClass("active-mapstyle"),$("#reset-all-station-styles").text("Set ALL stations to "+$(this).text()),activeMap&&activeMap.global&&activeMap.global.style?activeMap.global.style.mapStationStyle=mapStationStyle:activeMap&&activeMap.global&&(activeMap.global.style={mapStationStyle:mapStationStyle}),autoSave(activeMap),drawCanvas()})),$("#station-name").change((function(){$(this).val($(this).val().replaceAll('"',"").replaceAll("'","").replaceAll("<","").replaceAll(">","").replaceAll("&","").replaceAll("/","").replaceAll("_"," ").replaceAll("\\\\","").replaceAll("%",""));var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val();Object.keys(temporaryStation).length>0&&(2==mapDataVersion||3==mapDataVersion?(activeMap.stations||(activeMap.stations={}),activeMap.stations[e]||(activeMap.stations[e]={}),activeMap.stations[e][t]=Object.assign({},temporaryStation)):activeMap[e][t].station=Object.assign({},temporaryStation),temporaryStation={}),metroMap=updateMapObject(e,t,"name",$("#station-name").val()),autoSave(metroMap),drawCanvas(metroMap,!0)})),$("#station-name-orientation").change((function(){var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val(),i=parseInt($(this).val());e>=0&&t>=0&&(0==i?Object.keys(temporaryStation).length>0?temporaryStation.orientation=0:2==mapDataVersion||3==mapDataVersion?activeMap.stations[e][t].orientation=0:1==mapDataVersion&&(activeMap[e][t].station.orientation=0):ALLOWED_ORIENTATIONS.indexOf(i)>=0&&(Object.keys(temporaryStation).length>0?temporaryStation.orientation=i:2==mapDataVersion||3==mapDataVersion?activeMap.stations[e][t].orientation=i:1==mapDataVersion&&(activeMap[e][t].station.orientation=i))),window.localStorage.setItem("metroMapStationOrientation",i),0==Object.keys(temporaryStation).length&&autoSave(activeMap),drawCanvas(activeMap,!0),drawIndicator(e,t)})),$("#station-style").on("change",(function(){var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val(),i=$(this).val();e>=0&&t>=0&&(ALLOWED_STYLES.indexOf(i)>=0?Object.keys(temporaryStation).length>0?temporaryStation.style=i:2==mapDataVersion||3==mapDataVersion?activeMap.stations[e][t].style=i:1==mapDataVersion&&(activeMap[e][t].station.style=i):i||(2!=mapDataVersion&&3!=mapDataVersion||!activeMap.stations[e][t].style?1==mapDataVersion&&activeMap[e][t].station.style&&delete activeMap[e][t].station.style:delete activeMap.stations[e][t].style)),0==Object.keys(temporaryStation).length&&autoSave(activeMap),drawCanvas(activeMap,!0),drawIndicator(e,t)})),$("#station-transfer").click((function(){var e=$("#station-coordinates-x").val(),t=$("#station-coordinates-y").val();e>=0&&t>=0&&($(this).is(":checked")?Object.keys(temporaryStation).length>0?temporaryStation.transfer=1:2==mapDataVersion||3==mapDataVersion?activeMap.stations[e][t].transfer=1:1==mapDataVersion&&(activeMap[e][t].station.transfer=1):Object.keys(temporaryStation).length>0?delete temporaryStation.transfer:2==mapDataVersion||3==mapDataVersion?delete activeMap.stations[e][t].transfer:1==mapDataVersion&&delete activeMap[e][t].station.transfer),0==Object.keys(temporaryStation).length&&autoSave(activeMap),drawCanvas(activeMap,!0),drawIndicator(e,t)})),$("#loading").remove()})),$("#set-all-station-name-orientation").on("click",(function(){var e=$("#set-all-station-name-orientation-choice").val();setAllStationOrientations(activeMap,e),autoSave(activeMap),drawCanvas(),setTimeout((function(){$("#set-all-station-name-orientation").removeClass("active")}),500)})),$("#reset-all-station-styles").on("click",(function(){resetAllStationStyles(activeMap),autoSave(activeMap),drawCanvas(),setTimeout((function(){$("#reset-all-station-styles").removeClass("active")}),500)})),$("#try-on-mobile").click((function(){editOnSmallScreen()})),$("#i-am-on-desktop").on("click",(function(){editOnSmallScreen(),$("#tool-export-canvas").remove(),$("#tool-download-image").removeClass("hidden-xs")})),$("#controls-collapse-menu").on("click",collapseToolbox),$("#controls-expand-menu").on("click",expandToolbox),$(".line-style-choice-width").on("click",(function(){$(".line-style-choice-width").removeClass("active"),$(".line-style-choice-width.active-mapstyle").removeClass("active-mapstyle"),$(this).addClass("active-mapstyle"),$(this).addClass("active"),activeLineWidth=$(this).attr("data-linewidth")/100,activeLineWidthStyle=activeLineWidth+"-"+activeLineStyle,activeToolOption&&(activeTool="line")})),$(".line-style-choice-style").on("click",(function(){$(".line-style-choice-style").removeClass("active"),$(".line-style-choice-style.active-mapstyle").removeClass("active-mapstyle"),$(this).addClass("active-mapstyle"),$(this).addClass("active"),activeLineStyle=$(this).attr("data-linestyle"),activeLineWidthStyle=activeLineWidth+"-"+activeLineStyle,activeToolOption&&(activeTool="line")})),$("#label-text, #label-shape, #label-text-color, #label-bg-color-transparent, #label-bg-color").on("change",(function(){var e=$("#label-coordinates-x").val(),t=$("#label-coordinates-y").val();temporaryLabel.text=$("#label-text").val(),temporaryLabel.shape=$("#label-shape").val(),temporaryLabel["text-color"]=$("#label-text-color").val(),$("#label-bg-color-transparent").is(":checked")?temporaryLabel["bg-color"]=void 0:temporaryLabel["bg-color"]=$("#label-bg-color").val(),autoSave(activeMap=updateMapObject(e,t,!1,Object.assign({},temporaryLabel))),temporaryLabel={},drawCanvas(activeMap,!0),drawLabelIndicator(e,t)})),$("#tool-ruler").on("click",(function(){rulerOn=!rulerOn,rulerOrigin=[];var e=document.getElementById("ruler-canvas");e.getContext("2d").clearRect(0,0,e.width,e.height)})),$("#tool-undo").on("click",undo),$("#tool-redo").on("click",redo); \ No newline at end of file diff --git a/metro_map_saver/map_saver/templates/index.html b/metro_map_saver/map_saver/templates/index.html index 4759a708..4a1260ed 100644 --- a/metro_map_saver/map_saver/templates/index.html +++ b/metro_map_saver/map_saver/templates/index.html @@ -124,8 +124,10 @@
This works best on desk
+
+ @@ -157,10 +159,31 @@

+
+
+ Line Color +
+
+ +
+
+ Line Size (Keyboard shortcut: W) + {% for line_width in ALLOWED_LINE_WIDTHS %} + + {% endfor %} +
+
+ Line Style (Keyboard shortcut: Q) + {% for line_style in ALLOWED_LINE_STYLES %} + + {% endfor %} +
+
+ diff --git a/metro_map_saver/map_saver/templatetags/metromap_utils.py b/metro_map_saver/map_saver/templatetags/metromap_utils.py index 76dcee4f..f0534c3c 100644 --- a/metro_map_saver/map_saver/templatetags/metromap_utils.py +++ b/metro_map_saver/map_saver/templatetags/metromap_utils.py @@ -5,6 +5,7 @@ from map_saver.validator import ( ALLOWED_ORIENTATIONS, + ALLOWED_LINE_STYLES, ALLOWED_STATION_STYLES, ALLOWED_CONNECTING_STATIONS, is_hex, @@ -26,7 +27,7 @@ ] @register.simple_tag -def station_marker(station, default_shape, line_size, points_by_color, stations): +def station_marker(station, default_shape, line_size, points_by_color, stations, data_version): """ Generate the SVG shape for a station based on whether it's a transfer station and what its shape is. @@ -75,7 +76,16 @@ def station_marker(station, default_shape, line_size, points_by_color, stations) else: radius = False - line_direction = get_line_direction(x, y, color, points_by_color) + # What is the line width/style for this line? + if data_version >= 3: + line_width_style = station['line_width_style'] + line_size, line_style = line_width_style.split('-') + line_size = float(line_size) + else: + # line_width and style are set globally in data_version 2 + line_width_style = None + + line_direction = get_line_direction(x, y, color, points_by_color, line_width_style) station_direction = get_connected_stations(x, y, stations) draw_as_connected = False @@ -210,7 +220,7 @@ def svg_rect(x, y, w, h, x_offset, y_offset, fill, stroke=None, stroke_width=0.2 return f'' return f'' -def get_line_direction(x, y, color, points_by_color): +def get_line_direction(x, y, color, points_by_color, line_width_style=None): """ Returns which direction this line is going in, to help draw the positioning of rectangle stations @@ -218,14 +228,21 @@ def get_line_direction(x, y, color, points_by_color): color = color.removeprefix('#') - NW = (x-1, y-1) in points_by_color[color]['xy'] - NE = (x+1, y-1) in points_by_color[color]['xy'] - SW = (x-1, y+1) in points_by_color[color]['xy'] - SE = (x+1, y+1) in points_by_color[color]['xy'] - N = (x, y-1) in points_by_color[color]['xy'] - E = (x+1, y) in points_by_color[color]['xy'] - S = (x, y+1) in points_by_color[color]['xy'] - W = (x-1, y) in points_by_color[color]['xy'] + if not line_width_style: + # 'xy' isn't a valid value for a line width/style; + # but it's a compatibility measure for data_version == 2, + # which used that key in the same position as data_version == 3's + # line width and style. + line_width_style = 'xy' + + NW = (x-1, y-1) in points_by_color[color][line_width_style] + NE = (x+1, y-1) in points_by_color[color][line_width_style] + SW = (x-1, y+1) in points_by_color[color][line_width_style] + SE = (x+1, y+1) in points_by_color[color][line_width_style] + N = (x, y-1) in points_by_color[color][line_width_style] + E = (x+1, y) in points_by_color[color][line_width_style] + S = (x, y+1) in points_by_color[color][line_width_style] + W = (x-1, y) in points_by_color[color][line_width_style] if W and E: return 'horizontal' @@ -459,6 +476,8 @@ def station_text(station): else: x_val = station['xy'][0] + 0.75 + y_val = station['xy'][1] + if station['orientation'] == 0: # Right pass @@ -492,8 +511,22 @@ def station_text(station): transform = '' else: transform = f' transform="rotate({station["orientation"]} {station["xy"][0]}, {station["xy"][1]})"' + elif station['orientation'] == 1: + text_anchor = ' text-anchor="middle"' + x_val = station['xy'][0] + if station.get('transfer'): + y_val = y_val - 1.75 + else: + y_val = y_val - 1.25 + elif station['orientation'] == -1: + text_anchor = ' text-anchor="middle"' + x_val = station['xy'][0] + if station.get('transfer'): + y_val = y_val + 1.75 + else: + y_val = y_val + 1.25 - text = f'''''' + text = f'''''' return format_html( '{}{}{}', @@ -595,6 +628,66 @@ def get_station_styles_in_use(stations, default_shape, line_size): mark_safe(svg), ) +@register.simple_tag +def get_line_width_styles_for_svg_style(shapes_by_color): + + """ Given a set of shapes_by_color, + get the unique line widths and styles + """ + + widths = set() + styles = set() + + for color in shapes_by_color: + for width_style in shapes_by_color[color]: + width, style = width_style.split('-') + widths.add(width) + styles.add(style) + + css_styles = [] + for width in widths: + if width in SVG_STYLES: + css_styles.append(f".{SVG_STYLES[width]['class']} {{ {SVG_STYLES[width]['style']} }}") + + for style in styles: + if style in SVG_STYLES: + css_styles.append(f".{SVG_STYLES[style]['class']} {{ {SVG_STYLES[style]['style']} }}") + + css_styles = ''.join(css_styles) + + return format_html( + '{}', + mark_safe(css_styles), + ) + +@register.simple_tag +def get_line_class_from_width_style(width_style, line_size): + + """ Given a width_style and line_size, return the appropriate CSS class(es) + necessary for this line (if any) + """ + + classes = [] + + width, style = width_style.split('-') + line_size = str(line_size) + if width == line_size: + pass # No class necessary; it's the default + elif width in SVG_STYLES: + classes.append(SVG_STYLES[width]['class']) + + if style == ALLOWED_LINE_STYLES[0]: + pass # No class necessary; it's the default (solid) + elif style in SVG_STYLES: + classes.append(SVG_STYLES[style]['class']) + + classes = ' '.join(classes) + + return format_html( + '{}', + mark_safe(classes) + ) + @register.filter def square_root(value): return sqrt(value) @@ -632,6 +725,10 @@ def map_color(color, color_map): return color_map[color] +@register.filter +def underscore_to_space(value): + return value.replace('_', ' ') + SVG_DEFS = { 'wmata': { 'wm-xf': [ # WMATA transfer @@ -655,3 +752,15 @@ def map_color(color, color_map): } } +SVG_STYLES = { + 'dashed': {"class": "l1", "style": "stroke-dasharray: 1 1.5; stroke-linecap: square;"}, + 'dotted': {"class": "l2", "style": "stroke-dasharray: .5 .5; stroke-linecap: butt;"}, + 'dotted_dense': {"class": "l3", "style": "stroke-dasharray: .5 .25; stroke-linecap: butt;"}, + 'dense_thin': {"class": "l4", "style": "stroke-dasharray: .05 .05; stroke-linecap: butt;"}, + 'dense_thick': {"class": "l5", "style": "stroke-dasharray: .1 .1; stroke-linecap: butt;"}, + '1': {"class": "w1", "style": "stroke-width: 1;"}, + '0.75': {"class": "w2", "style": "stroke-width: .75;"}, + '0.5': {"class": "w3", "style": "stroke-width: .5;"}, + '0.25': {"class": "w4", "style": "stroke-width: .25;"}, + '0.125': {"class": "w5", "style": "stroke-width: .125;"}, +} diff --git a/metro_map_saver/map_saver/validator.py b/metro_map_saver/map_saver/validator.py index 404a0b5e..c8c2608c 100644 --- a/metro_map_saver/map_saver/validator.py +++ b/metro_map_saver/map_saver/validator.py @@ -13,11 +13,25 @@ MAX_MAP_SIZE = ALLOWED_MAP_SIZES[-1] VALID_XY = [str(x) for x in range(MAX_MAP_SIZE)] ALLOWED_LINE_WIDTHS = [1, 0.75, 0.5, 0.25, 0.125] +ALLOWED_LINE_STYLES = ['solid', 'dashed', 'dense_thin', 'dense_thick', 'dotted_dense', 'dotted'] ALLOWED_STATION_STYLES = ['wmata', 'rect', 'rect-round', 'circles-lg', 'circles-md', 'circles-sm', 'circles-thin'] -ALLOWED_ORIENTATIONS = [0, 45, -45, 90, -90, 135, -135, 180] +ALLOWED_ORIENTATIONS = [0, 45, -45, 90, -90, 135, -135, 180, 1, -1] ALLOWED_CONNECTING_STATIONS = ['rect', 'rect-round', 'circles-thin'] ALLOWED_TAGS = ['real', 'speculative', 'unknown'] # TODO: change 'speculative' to 'fantasy' here and everywhere else, it's the more common usage +# TODO: ALLOWED_LABEL DETAILS +# ALLOWED_LABEL_SHAPES = [] +# ALLOWED_LABEL_TEXT_LENGTH = {} +# ALLOWED_LABEL_TEXT_COLORS = [] +# ALLOWED_LABEL_BG_COLORS = [] + +ALLOWED_LINE_WIDTH_STYLES = [] +for allowed_width in ALLOWED_LINE_WIDTHS: + for allowed_style in ALLOWED_LINE_STYLES: + # Yes, I can use this in an import; + # ALLOWED_LINE_WIDTH_STYLES has been populated + ALLOWED_LINE_WIDTH_STYLES.append(f'{allowed_width}-{allowed_style}') + def is_hex(string): """ Determines whether a string is a hexademical string (0-9, a-f) or not @@ -97,6 +111,250 @@ def get_map_size(highest_xy_seen): return allowed_size return ALLOWED_MAP_SIZES[-1] +def validate_metro_map_v3(metro_map): + + """ Validate the MetroMap object, allowing mixing and matching line widths/styles. + + Main difference from v2 is points by color has an additional layer: the width+style, + and drops the xy/xys intermediate key + """ + + validated_metro_map = { + 'global': { + 'data_version': 3, + 'lines': {}, + 'style': {}, + }, + 'points_by_color': {}, + 'stations': {}, + } + + if not metro_map.get('points_by_color'): + raise ValidationError(f"[VALIDATIONFAILED] 3-01: No points_by_color") + + if not isinstance(metro_map['points_by_color'], dict): + raise ValidationError(f"[VALIDATIONFAILED] 3-02: points_by_color must be dict, is: {type(metro_map['points_by_color']).__name__}") + + # Infer missing lines in global from points_by_color + # It's not pretty, and the lines could fail to validate for other reasons, but it's graceful. + inferred_lines = False + if not metro_map.get('global') or not metro_map.get('global', {}).get('lines'): + inferred_lines = True + metro_map['global'] = { + 'lines': { + color: {'displayName': color} + for color in metro_map['points_by_color'] + } + } + else: + if not isinstance(metro_map['global']['lines'], dict): + metro_map['global']['lines'] = {} + if set(metro_map['global']['lines'].keys()) != set(metro_map['points_by_color'].keys()): + for color in metro_map['points_by_color']: + if color in metro_map['global']['lines']: + continue + metro_map['global']['lines'][color] = {'displayName': color} + + valid_lines = [] + # Allow HTML color names to be used, but convert them to hex values + metro_map['global']['lines'] = { + html_color_name_fragments.get(line.strip()) or line: data + for line, data in + metro_map['global']['lines'].items() + } + + invalid_lines = [] + + remove_lines = {} + for line in metro_map['global']['lines']: + if not is_hex(line): + # Allow malformed invalid lines to be skipped so the rest of the map will validate + invalid_lines.append(line) + if not len(line) == 6: + # We know it's hex by this point, we can fix length + if len(line) == 3: + new_line = ''.join([line[0] * 2, line[1] * 2, line[2] * 2]) + elif len((line * 6)[:6]) <= 6: + new_line = (line * 6)[:6] + else: + new_line = line[:6] + remove_lines[new_line] = line + + for new_line, line in remove_lines.items(): + metro_map['global']['lines'][new_line] = metro_map['global']['lines'].pop(line) + + for line in metro_map['global']['lines']: + + if line in invalid_lines: + continue + + # Transformations to the display name could result in a non-unique display name, but it doesn't actually matter. + display_name = metro_map['global']['lines'][line].get('displayName', 'Rail Line') + if not isinstance(display_name, str) or len(display_name) < 1: + display_name = 'Rail Line' + elif len(display_name) > 255: + display_name = display_name[:255] + + valid_lines.append(line) + validated_metro_map['global']['lines'][line] = { + 'displayName': sanitize_string(display_name) + } + + line_width = metro_map['global'].get('style', {}).get('mapLineWidth', 1) + if line_width not in ALLOWED_LINE_WIDTHS: + line_width = ALLOWED_LINE_WIDTHS[0] + + line_style = metro_map['global'].get('style', {}).get('mapLineStyle', 'solid') + if line_style not in ALLOWED_LINE_STYLES: + line_style = ALLOWED_LINE_STYLES[0] + + station_style = metro_map['global'].get('style', {}).get('mapStationStyle', 'wmata') + if station_style not in ALLOWED_STATION_STYLES: + station_style = ALLOWED_STATION_STYLES[0] + + validated_metro_map['global']['style'] = { + 'mapLineWidth': line_width, + 'mapLineStyle': line_style, + 'mapStationStyle': station_style, + } + + # Points by Color + all_points_seen = set() # Must confirm that stations exist on these points + points_skipped = [] + highest_xy_seen = -1 # Because 0 is a point + valid_points_by_color = {} + for color in metro_map['points_by_color']: + if color not in validated_metro_map['global']['lines']: + points_skipped.append(f'Color {color} not in global') + continue + + if not isinstance(metro_map['points_by_color'][color], dict): + points_skipped.append(f'BAD LINE WIDTH/STYLE at {color} (non-dict)') + continue + + for line_width_style in metro_map['points_by_color'][color]: + + if not isinstance(metro_map['points_by_color'][color][line_width_style], dict): + points_skipped.append(f'BAD COORDS at {color} for {line_width_style}') + continue + + if line_width_style not in ALLOWED_LINE_WIDTH_STYLES: + points_skipped.append(f'BAD LINE WIDTH/STYLE at {color}: {line_width_style}') + + for x in metro_map['points_by_color'][color][line_width_style]: + if not isinstance(metro_map['points_by_color'][color][line_width_style][x], dict): + points_skipped.append(f'BAD X at {color}: {x}') + continue + + if not x.isdigit(): + points_skipped.append(f'NONINT X at {color}: {x}') + continue + + if int(x) < 0 or int(x) >= MAX_MAP_SIZE: + points_skipped.append(f'OOB X at {color}: {x}') + continue + + for y in metro_map['points_by_color'][color][line_width_style][x]: + + if not y.isdigit(): + points_skipped.append(f'NONINT Y at {color}: {x},{y}') + continue + + if int(y) < 0 or int(y) >= MAX_MAP_SIZE: + points_skipped.append(f'OOB Y at {color}: {x},{y}') + continue + + if (x, y) in all_points_seen: + # Already seen in another color + points_skipped.append(f'SKIPPING {color} POINT {x},{y}, ALREADY SEEN') + continue + + if metro_map['points_by_color'][color][line_width_style][x][y] == 1: + # Originally I'd considered setting the line width / style at the [x][y], + # but I think it's better recorded at points_by_color[color][line_width_style] + all_points_seen.add((x, y)) + + if int(x) > highest_xy_seen: + highest_xy_seen = int(x) + + if int(y) > highest_xy_seen: + highest_xy_seen = int(y) + + if not valid_points_by_color.get(color): + valid_points_by_color[color] = {line_width_style: {}} + elif not valid_points_by_color[color].get(line_width_style): + valid_points_by_color[color][line_width_style] = {} + + if not valid_points_by_color[color][line_width_style].get(x): + valid_points_by_color[color][line_width_style][x] = {} + + valid_points_by_color[color][line_width_style][x][y] = 1 + + validated_metro_map['points_by_color'] = valid_points_by_color + if points_skipped: + logger.warn(f'Points skipped: {len(points_skipped)} Details: {points_skipped}') + + # Stations + stations_skipped = [] + valid_stations = {} + if metro_map.get('stations') and isinstance(metro_map['stations'], dict): + for x in metro_map['stations']: + if not isinstance(metro_map['stations'][x], dict): + stations_skipped.append(f'STA BAD X: {x}') + continue + for y in metro_map['stations'][x]: + if not isinstance(metro_map['stations'][x][y], dict): + stations_skipped.append(f'STA BAD Y: {y}') + continue + + if (x, y) not in all_points_seen: + stations_skipped.append(f'STA BAD POS: {x},{y}') + continue + + station_name = sanitize_string_without_html_entities(metro_map['stations'][x][y].get('name', '_') or '_') + if len(station_name) < 1: + station_name = '_' + elif len(station_name) > 255: + station_name = station_name[:255] + + station = {'name': station_name} + + try: + station_orientation = int(metro_map['stations'][x][y].get('orientation', ALLOWED_ORIENTATIONS[0])) + except Exception: + station_orientation = ALLOWED_ORIENTATIONS[0] + + if station_orientation not in ALLOWED_ORIENTATIONS: + station_orientation = ALLOWED_ORIENTATIONS[0] + station['orientation'] = station_orientation + + station_style = metro_map['stations'][x][y].get('style') + if station_style and station_style in ALLOWED_STATION_STYLES: + station['style'] = station_style + + if metro_map['stations'][x][y].get('transfer'): + station['transfer'] = 1 + + # This station is valid, add it + if not valid_stations.get(x): + valid_stations[x] = {} + + valid_stations[x][y] = station + validated_metro_map['stations'] = valid_stations + + if stations_skipped: + logger.warn(f'Stations skipped: {len(stations_skipped)} Details: {stations_skipped}') + + # TODO: Add support for labels + + if highest_xy_seen == -1: + raise ValidationError(f"[VALIDATIONFAILED] 3-00: This map has no points drawn. If this is in error, please contact the admin.") + + validated_metro_map['global']['map_size'] = get_map_size(highest_xy_seen) + + return validated_metro_map + + def validate_metro_map_v2(metro_map): """ Validate the MetroMap object, with a more compact, optimized data representation diff --git a/metro_map_saver/map_saver/views.py b/metro_map_saver/map_saver/views.py index 29ba7216..d697883c 100644 --- a/metro_map_saver/map_saver/views.py +++ b/metro_map_saver/map_saver/views.py @@ -49,6 +49,8 @@ validate_metro_map, hex64, ALLOWED_TAGS, + ALLOWED_LINE_WIDTHS, + ALLOWED_LINE_STYLES, ALLOWED_MAP_SIZES, ) from .common_cities import CITIES @@ -115,6 +117,8 @@ def get(self, request, **kwargs): context['today'] = timezone.now().date() context['ALLOWED_MAP_SIZES'] = ALLOWED_MAP_SIZES + context['ALLOWED_LINE_WIDTHS'] = [w * 100 for w in ALLOWED_LINE_WIDTHS] + context['ALLOWED_LINE_STYLES'] = ALLOWED_LINE_STYLES return render(request, self.template_name, context) @@ -609,7 +613,7 @@ def post(self, request, **kwargs): 'stations': ','.join(stations), 'map_size': mapdata.get('global', {}).get('map_size', -1) or -1, } - if data_version == 2: + if data_version >= 2: map_details['data'] = mapdata else: map_details['mapdata'] = json.dumps(mapdata) @@ -617,8 +621,6 @@ def post(self, request, **kwargs): saved_map = SavedMap.objects.create(**map_details) context['saved_map'] = f'{urlhash},{naming_token}' except MultipleObjectsReturned: - # This should never happen, but it happened once - # Perhaps this was due to a race condition? context['saved_map'] = f'{urlhash},' else: # Anything that appears before the first colon will be internal-only; diff --git a/metro_map_saver/summary/management/commands/summarize_city.py b/metro_map_saver/summary/management/commands/summarize_city.py index 23f9bf07..e570a87a 100644 --- a/metro_map_saver/summary/management/commands/summarize_city.py +++ b/metro_map_saver/summary/management/commands/summarize_city.py @@ -38,6 +38,8 @@ def handle(self, *args, **kwargs): # Find maps that don't have city set, but do have either a name or suggested_city maps = SavedMap.objects.filter(city=None).exclude((Q(name='') & Q(suggested_city=''))) + # TODO: this is all pretty slow and could be sped up quite a bit + if not alltime: since = datetime.date.today() - datetime.timedelta(days=7) maps = maps.filter(created_at__date__gte=since) diff --git a/metro_map_saver/summary/views.py b/metro_map_saver/summary/views.py index 805e547f..dcb3105c 100644 --- a/metro_map_saver/summary/views.py +++ b/metro_map_saver/summary/views.py @@ -265,7 +265,7 @@ def get_context_data(self, **kwargs): return context def post(self, request, *args, **kwargs): - city = request.POST.get('city') + city = request.POST.get('city', '').replace('/', ' ') if city: return HttpResponseRedirect(reverse_lazy('city', args=(city, ))) else: diff --git a/requirements.txt b/requirements.txt index b566ac2f..f507acc9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -asgiref==3.7.2 -Django==5.0.1 +asgiref==3.8.1 +Django==5.1.2 django-debug-toolbar==4.2.0 django-taggit==5.0.1 mysqlclient==2.2.1