diff options
author | notoraptor <stevenbocco@gmail.com> | 2019-08-14 12:22:22 -0400 |
---|---|---|
committer | Philip Paquette <pcpaquette@gmail.com> | 2019-08-14 12:40:01 -0400 |
commit | 5c3bd9b3802e2001a7e77baf2911386135a03839 (patch) | |
tree | e641744650b05cddc85bc60c2d7e2d6fe2d88b47 /diplomacy/web/svg_to_react.py | |
parent | 5acb4ff23be4757a49b234f93928f13c436b60c6 (diff) |
[Web] Integrated new maps on the web interface
- Fixed bug with incorrect dislodged unit on pure map
- [python] Make sure dummy powers are registered only for standard maps.
- Hardcoded supply centers into SVG files.
- Removed supply centers CSS classes.
- Update positions for units and dislodged units on all maps.
- Converted SVGs to React.
- Removed "sym" classes and hardcode related styles into symbol definitions.
- Reordered map list (standard at top, then other ones in alphabetical order)
- Displayed + button for all maps and disable it for maps without variants.
- Minified generated code when converting SVG files to React.
- [web] Added ability to hide/display map abbreviations.
Diffstat (limited to 'diplomacy/web/svg_to_react.py')
-rw-r--r-- | diplomacy/web/svg_to_react.py | 640 |
1 files changed, 556 insertions, 84 deletions
diff --git a/diplomacy/web/svg_to_react.py b/diplomacy/web/svg_to_react.py index 12a68b6..dafe4d3 100644 --- a/diplomacy/web/svg_to_react.py +++ b/diplomacy/web/svg_to_react.py @@ -19,11 +19,122 @@ """ import argparse import os -import sys +import re from xml.dom import minidom, Node import ujson as json +LICENSE_TEXT = """/** +============================================================================== +Copyright (C) 2019 - Philip Paquette, Steven Bocco + + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see <https:www.gnu.org/licenses/>. +============================================================================== +**/""" + +TAG_ORDERDRAWING = 'jdipNS:ORDERDRAWING' +TAG_POWERCOLORS = 'jdipNS:POWERCOLORS' +TAG_POWERCOLOR = 'jdipNS:POWERCOLOR' +TAG_SYMBOLSIZE = 'jdipNS:SYMBOLSIZE' +TAG_PROVINCE_DATA = 'jdipNS:PROVINCE_DATA' +TAG_PROVINCE = 'jdipNS:PROVINCE' +TAG_UNIT = 'jdipNS:UNIT' +TAG_DISLODGED_UNIT = 'jdipNS:DISLODGED_UNIT' +TAG_SUPPLY_CENTER = 'jdipNS:SUPPLY_CENTER' + +SELECTOR_REGEX = re.compile(r'([\r\n][ \t]*)([^{\r\n]+){') +LINES_REGEX = re.compile(r'[\r\n]+') +SPACES_REGEX = re.compile(r'[\t ]+') +STRING_REGEX = re.compile(r'[`\'"] {0,1}\+ {0,1}[`\'"]') + + +def prepend_css_selectors(prefix, css_text): + """ Prepend all CSS selector with given prefix (e.g. ID selector) followed by a space. + :param prefix: prefix to prepend + :param css_text: CSS text to parse + :rtype: str + """ + def repl(match): + return '%s%s %s{' % (match.group(1), prefix, match.group(2)) + + return SELECTOR_REGEX.sub(repl, css_text) + + +class ExtractedData: + """ Helper class to store extra data collected while parsing SVG file. Properties: + - name: class name of parsed SVG component + - extra: data parsed from invalid tags found in SVG content + - style_lines: string lines parsed from <style> tag if found in SVG content + - id_to_class: dictionary mapping and ID to corresponding class name + for each tag found with both ID and class name in SVG content. + """ + __slots__ = ('name', 'extra', 'style_lines', 'id_to_class') + + def __init__(self, name): + """ Initialize extracted data object. + :param name: class name of parsed SVG content + """ + self.name = name + self.extra = {} + self.style_lines = [] + self.id_to_class = {} + + def get_coordinates(self): + """ Parse and return unit coordinates from extra field. + :return: a dictionary mapping a province name to coordinates [x, y] (as string values) + for unit ('unit'), dislodged unit ('disl'), and supply center ('sc', if available). + :rtype: dict + """ + coordinates = {} + for province_definition in self.extra[TAG_PROVINCE_DATA][TAG_PROVINCE]: + name = province_definition['name'].upper().replace('-', '/') + coordinates[name] = {} + if TAG_UNIT in province_definition: + coordinates[name]['unit'] = [ + province_definition[TAG_UNIT]['x'], province_definition[TAG_UNIT]['y']] + if TAG_DISLODGED_UNIT in province_definition: + coordinates[name]['disl'] = [province_definition[TAG_DISLODGED_UNIT]['x'], + province_definition[TAG_DISLODGED_UNIT]['y']] + if TAG_SUPPLY_CENTER in province_definition: + coordinates[name]['sc'] = [province_definition[TAG_SUPPLY_CENTER]['x'], + province_definition[TAG_SUPPLY_CENTER]['y']] + return coordinates + + def get_symbol_sizes(self): + """ Parse and return symbol sizes from extra field. + :return: a dictionary mapping a symbol name to sizes + ('width' and 'height' as floating values). + :rtype: dict + """ + sizes = {} + for definition in self.extra[TAG_ORDERDRAWING][TAG_SYMBOLSIZE]: + sizes[definition['name']] = { + 'width': float(definition['width']), + 'height': float(definition['height']) + } + return sizes + + def get_colors(self): + """ Parse and return power colors from extra field. + :return: a dictionary mapping a power name to a HTML color. + :rtype: dict + """ + colors = {} + for definition in self.extra[TAG_ORDERDRAWING][TAG_POWERCOLORS][TAG_POWERCOLOR]: + colors[definition['power'].upper()] = definition['color'] + return colors + def safe_react_attribute_name(name): """ Convert given raw attribute name into a valid React HTML tag attribute name. @@ -42,6 +153,8 @@ def safe_react_attribute_name(name): for piece in input_pieces[1:]: output_pieces.append('%s%s' % (piece[0].upper(), piece[1:])) return ''.join(output_pieces) + if name == 'xlink:href': + return 'href' # Otherwise, return name as-is. return name @@ -51,6 +164,7 @@ def compact_extra(extra): :param extra: dictionary of extra data :type extra: dict """ + # pylint:disable=too-many-branches if 'children' in extra: names = set() text_found = False @@ -60,7 +174,8 @@ def compact_extra(extra): else: names.add(child['name']) if len(names) == len(extra['children']): - # Each child has a different name, so they cannot be confused, and extra dictionary can be merged with them. + # Each child has a different name, so they cannot be confused, + # and extra dictionary can be merged with them. children_dict = {} for child in extra['children']: child_name = child.pop('name') @@ -83,7 +198,8 @@ def compact_extra(extra): compact_extra(child) extra[name] = child else: - # We found many children with same name. Merge them as a list into extra dictionary. + # We found many children with same name. + # Merge them as a list into extra dictionary. values = [] for child in children: child.pop('name') @@ -117,7 +233,7 @@ def extract_extra(node, extra): text = child.data.strip() if text: extra_dictionary['children'].append(text) - else: + elif child.nodeType != Node.COMMENT_NODE: # Child is a normal node. We still consider it as an extra node. extract_extra(child, extra_dictionary) # Save extra node data into list field extra['children']. @@ -141,28 +257,25 @@ def attributes_to_string(attributes): return ' '.join(pieces) -def extract_dom(node, nb_indentation, lines, extra, style_lines, id_to_class, identifiers_to_remove, action_parents): +def extract_dom(node, nb_indentation, lines, data): """ Parse given node. :param node: (input) node to parse - :param nb_indentation: (input) number of indentation to use for current node content into output lines - 1 indentation is converted to 4 spaces. - :param lines: (output) lines to collect output lines of text corresponding to parsed content - :param extra: (output) dictionary to collect extra data (corresponding to invalid/unhandled tags( - :param style_lines: (output) lines to collect output lines of CSS file corresponding to `style` tag (if found) + :param nb_indentation: (input) number of indentation to use for current node content + into output lines. 1 indentation is converted to 4 spaces. + :param lines: (output) collector for output lines of text corresponding to parsed content + :param data: ExtractedData object to collect extracted data + (extra, style lines, ID-to-class mapping). :type nb_indentation: int :type lines: List[str] - :type extra: dict - :type style_lines: List[str] - :type id_to_class: dict - :type identifiers_to_remove: Iterable[str] - :type action_parents: Iterable[str] + :type data: ExtractedData """ + # pylint: disable=too-many-branches, too-many-statements if node.nodeType != Node.ELEMENT_NODE: return - tag_name = node.tagName - if ':' in tag_name: - # Found unhandled tag (example: `<jdipNS:DISPLAY>`). Collect it (and all its descendants) into extra. - extract_extra(node, extra) + if ':' in node.tagName: + # Found unhandled tag (example: `<jdipNS:DISPLAY>`). + # Collect it (and all its descendants) into extra. + extract_extra(node, data.extra) else: # Found valid tag. attributes = {} @@ -181,14 +294,11 @@ def extract_dom(node, nb_indentation, lines, extra, style_lines, id_to_class, id elif attribute_name == 'className': node_class = attribute.value if node_id: - if identifiers_to_remove and node_id in identifiers_to_remove: - # This node must be skipped. - return if node_class: # We parameterize class name for this node. attributes['className'] = "{classes['%s']}" % node_id - id_to_class[node_id] = node_class - if node.parentNode.getAttribute('id') in action_parents: + data.id_to_class[node_id] = node_class + if node.parentNode.getAttribute('id') == 'MouseLayer': # This node must react to onClick and onMouseOver. attributes['onClick'] = '{this.onClick}' attributes['onMouseOver'] = '{this.onHover}' @@ -201,34 +311,74 @@ def extract_dom(node, nb_indentation, lines, extra, style_lines, id_to_class, id child_lines.append(text) else: # Found an element node. - extract_dom(child, nb_indentation + 1, child_lines, extra, style_lines, - id_to_class, identifiers_to_remove, action_parents) - if tag_name == 'style': + extract_dom(child, nb_indentation + 1, child_lines, data) + if node.tagName == 'style': # Found 'style' tag. Save its children lines into style lines and return immediately, - style_lines.extend(child_lines) + data.style_lines.extend(child_lines) return + if node.tagName == 'svg': + if node_class: + attributes['className'] += ' %s' % data.name + else: + attributes['className'] = data.name + if node_id: + if not child_lines: + if node_id == 'Layer2': + child_lines.append('{renderedOrders2}') + elif node_id == 'Layer1': + child_lines.append('{renderedOrders}') + elif node_id == 'UnitLayer': + child_lines.append('{renderedUnits}') + elif node_id == 'DislodgedUnitLayer': + child_lines.append('{renderedDislodgedUnits}') + elif node_id == 'HighestOrderLayer': + child_lines.append('{renderedHighestOrders}') + elif node_id == 'CurrentNote': + child_lines.append("{nb_centers_per_power ? nb_centers_per_power : ''}") + elif node_id == 'CurrentNote2': + child_lines.append("{note ? note : ''}") + if (node_id == 'CurrentPhase' + and len(child_lines) == 1 + and isinstance(child_lines[0], str)): + child_lines = ['{current_phase}'] # We have a normal element node (not style node). Convert it to output lines. indentation = ' ' * (4 * nb_indentation) - attributes_string = attributes_to_string(attributes) + attr_string = attributes_to_string(attributes) if child_lines: # Node must be written as an open tag. if len(child_lines) == 1: # If we just have 1 child line, write a compact line. lines.append( '%s<%s%s>%s</%s>' % ( - indentation, tag_name, (' %s' % attributes_string) if attributes_string else '', + indentation, node.tagName, + (' %s' % attr_string) if attr_string else '', child_lines[0].lstrip(), - tag_name)) + node.tagName)) else: # Otherwise, write node normally. lines.append( - '%s<%s%s>' % (indentation, tag_name, (' %s' % attributes_string) if attributes_string else '')) + '%s<%s%s>' % (indentation, node.tagName, + (' %s' % attr_string) if attr_string else '')) lines.extend(child_lines) - lines.append('%s</%s>' % (indentation, tag_name)) + lines.append('%s</%s>' % (indentation, node.tagName)) else: # Node can be written as a close tag. lines.append( - '%s<%s%s/>' % (indentation, tag_name, (' %s' % attributes_string) if attributes_string else '')) + '%s<%s%s/>' % ( + indentation, node.tagName, (' %s' % attr_string) if attr_string else '')) + + +def to_json_string(dictionary): + """ Converts to a JSON string, without escaping the '/' characters """ + return json.dumps(dictionary).replace(r'\/', r'/') + + +def minify(code): + """ Minifyies a Javascript / CSS file """ + code = LINES_REGEX.sub(' ', code) + code = SPACES_REGEX.sub(' ', code) + code = STRING_REGEX.sub(' ', code) + return code def main(): @@ -240,55 +390,63 @@ def main(): parser.add_argument('--name', '-n', type=str, required=True, help="Component name.") parser.add_argument('--output', '-o', type=str, default=os.getcwd(), help='Output folder (default to working folder).') - parser.add_argument('--remove', '-r', action='append', help='(optional) Identifiers of nodes to remove') - parser.add_argument('--actionable', '-a', action='append', - help='(optional) Identifiers for which ' - 'all immediate children must have onClick and onMouseOver.') args = parser.parse_args() root = minidom.parse(args.input).documentElement class_name = args.name output_folder = args.output - identifiers_to_remove = set(args.remove) if args.remove else set() - action_parents = set(args.actionable) if args.actionable else set() + if not os.path.exists(output_folder): + os.makedirs(output_folder) assert os.path.isdir(output_folder), 'Not a directory: %s' % output_folder - extra_class_name = '%sExtra' % class_name + extra_class_name = '%sMetadata' % class_name lines = [] - extra = {} - style_lines = [] - id_to_class = {} - extract_dom(root, 3, lines, extra, style_lines, id_to_class, identifiers_to_remove, action_parents) - compact_extra(extra) + data = ExtractedData(class_name) + extract_dom(root, 3, lines, data) + compact_extra(data.extra) output_file_name = os.path.join(output_folder, '%s.js' % class_name) style_file_name = os.path.join(output_folder, '%s.css' % class_name) - extra_file_name = os.path.join(output_folder, '%s.js' % extra_class_name) + extra_parsed_file_name = os.path.join(output_folder, '%s.js' % extra_class_name) - if style_lines: + # CSS + if data.style_lines: with open(style_file_name, 'w') as style_file: - style_file.writelines(style_lines) + style_file.write(LICENSE_TEXT) + style_file.write('\n') + style_file.writelines( + minify(prepend_css_selectors('.%s' % class_name, '\n'.join(data.style_lines)))) - if extra: - with open(extra_file_name, 'w') as extra_file: - extra_file.write("""export const %(extra_class_name)s = %(extra_content)s;""" % { - 'extra_class_name': extra_class_name, - 'extra_content': json.dumps(extra, indent=4) - }) + # Metadata + if data.extra: + with open(extra_parsed_file_name, 'w') as extra_parsed_file: + extra_parsed_file.write("""%(license_text)s +export const Coordinates = %(coordinates)s; +export const SymbolSizes = %(symbol_sizes)s; +export const Colors = %(colors)s; +""" % {'license_text': LICENSE_TEXT, + 'coordinates': to_json_string(data.get_coordinates()), + 'symbol_sizes': to_json_string(data.get_symbol_sizes()), + 'colors': to_json_string(data.get_colors())}) - with open(output_file_name, 'w') as file: - file.write("""/** Generated using %(program_name)s with parameters: -%(args)s -**/ + # Map javacript + map_js_code = (""" import React from 'react'; import PropTypes from 'prop-types'; %(style_content)s %(extra_content)s - -function getClickedID(event) { - let node = event.target; - if (!node.id && node.parentNode.id && node.parentNode.tagName === 'g') - node = node.parentNode; - return node.id; -} +import {getClickedID, parseLocation, setInfluence} from "../common/common"; +import {Game} from "../../../diplomacy/engine/game"; +import {MapData} from "../../utils/map_data"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import {Diplog} from "../../../diplomacy/utils/diplog"; +import {extendOrderBuilding} from "../../utils/order_building"; +import {Unit} from "../common/unit"; +import {Hold} from "../common/hold"; +import {Move} from "../common/move"; +import {SupportMove} from "../common/supportMove"; +import {SupportHold} from "../common/supportHold"; +import {Convoy} from "../common/convoy"; +import {Build} from "../common/build"; +import {Disband} from "../common/disband"; export class %(classname)s extends React.Component { constructor(props) { @@ -297,40 +455,354 @@ export class %(classname)s extends React.Component { this.onHover = this.onHover.bind(this); } onClick(event) { - if (this.props.onClick) { - const id = getClickedID(event); - if (id) { - this.props.onClick(id); + if (this.props.orderBuilding) + return this.handleClickedID(getClickedID(event)); + } + onHover(event) { + return this.handleHoverID(getClickedID(event)); + } + handleClickedID(id) { + const orderBuilding = this.props.orderBuilding; + if (!orderBuilding.builder) + return this.props.onError('No orderable locations.'); + const province = this.props.mapData.getProvince(id); + if (!province) + throw new Error(`Cannot find a province named ${id}`); + + const stepLength = orderBuilding.builder.steps.length; + if (orderBuilding.path.length >= stepLength) + throw new Error(`Order building: current steps count (${orderBuilding.path.length}) should be less than` + + ` expected steps count (${stepLength}) (${orderBuilding.path.join(', ')}).`); + + const lengthAfterClick = orderBuilding.path.length + 1; + let validLocations = []; + const testedPath = [orderBuilding.type].concat(orderBuilding.path); + const value = UTILS.javascript.getTreeValue(this.props.game.ordersTree, testedPath); + if (value !== null) { + const checker = orderBuilding.builder.steps[lengthAfterClick - 1]; + try { + const possibleLocations = checker(province, orderBuilding.power); + for (let possibleLocation of possibleLocations) { + possibleLocation = possibleLocation.toUpperCase(); + if (value.includes(possibleLocation)) + validLocations.push(possibleLocation); + } + } catch (error) { + return this.props.onError(error); } } + if (!validLocations.length) + return this.props.onError('Disallowed.'); + + if (validLocations.length > 1 && orderBuilding.type === 'S' && orderBuilding.path.length >= 2) { + /* We are building a support order and we have a multiple choice for a location. */ + /* Let's check if next location to choose is a coast. To have a coast: */ + /* - all possible locations must start with same 3 characters. */ + /* - we expect at least province name in possible locations (e.g. 'SPA' for 'SPA/NC'). */ + /* If we have a coast, we will remove province name from possible locations. */ + let isACoast = true; + let validLocationsNoProvinceName = []; + for (let i = 0; i < validLocations.length; ++i) { + let location = validLocations[i]; + if (i > 0) { + /* Compare 3 first letters with previous location. */ + if (validLocations[i - 1].substring(0, 3).toUpperCase() !== validLocations[i].substring(0, 3).toUpperCase()) { + /* No same prefix with previous location. We does not have a coast. */ + isACoast = false; + break; + } + } + if (location.length !== 3) + validLocationsNoProvinceName.push(location); + } + if (validLocations.length === validLocationsNoProvinceName.length) { + /* We have not found province name. */ + isACoast = false; + } + if (isACoast) { + /* We want to choose location in a coastal province. Let's remove province name. */ + validLocations = validLocationsNoProvinceName; + } + } + + if (validLocations.length > 1) { + if (this.props.onSelectLocation) { + return this.props.onSelectLocation(validLocations, orderBuilding.power, orderBuilding.type, orderBuilding.path); + } else { + Diplog.warn(`Forced to select first valid location.`); + validLocations = [validLocations[0]]; + } + } + let orderBuildingType = orderBuilding.type; + if (lengthAfterClick === stepLength && orderBuildingType === 'M') { + const moveOrderPath = ['M'].concat(orderBuilding.path, validLocations[0]); + const moveTypes = UTILS.javascript.getTreeValue(this.props.game.ordersTree, moveOrderPath); + if (moveTypes !== null) { + if (moveTypes.length === 2 && this.props.onSelectVia) { + /* This move can be done either regularly or VIA a fleet. Let user choose. */ + return this.props.onSelectVia(validLocations[0], orderBuilding.power, orderBuilding.path); + } else { + orderBuildingType = moveTypes[0]; + } + } + } + extendOrderBuilding( + orderBuilding.power, orderBuildingType, orderBuilding.path, validLocations[0], + this.props.onOrderBuilding, this.props.onOrderBuilt, this.props.onError + ); } - onHover(event) { + handleHoverID(id) { if (this.props.onHover) { - const id = getClickedID(event); - if (id) { - this.props.onHover(id); + const province = this.props.mapData.getProvince(id); + if (province) { + this.props.onHover(province.name, this.getRelatedOrders(province.name)); } } } + getRelatedOrders(name) { + const orders = []; + if (this.props.orders) { + for (let powerOrders of Object.values(this.props.orders)) { + if (powerOrders) { + for (let order of powerOrders) { + const pieces = order.split(/ +/); + if (pieces[1].slice(0, 3) === name.toUpperCase().slice(0, 3)) + orders.push(order); + } + } + } + } + return orders; + } + getNeighbors(extraLocation) { + const selectedPath = [this.props.orderBuilding.type].concat(this.props.orderBuilding.path); + if (extraLocation) + selectedPath.push(extraLocation); + const possibleNeighbors = UTILS.javascript.getTreeValue(this.props.game.ordersTree, selectedPath); + const neighbors = possibleNeighbors ? possibleNeighbors.map(neighbor => parseLocation(neighbor)) : []; + return neighbors.length ? neighbors: null; + } render() { const classes = %(classes)s; + const game = this.props.game; + const mapData = this.props.mapData; + const orders = this.props.orders; + + /* Current phase. */ + const current_phase = (game.phase[0] === '?' || game.phase === 'COMPLETED') ? 'FINAL' : game.phase; + + /* Notes. */ + const nb_centers = []; + for (let power of Object.values(game.powers)) { + if (!power.isEliminated()) + nb_centers.push([power.name.substr(0, 3), power.centers.length]); + } + /* Sort nb_centers by descending number of centers. */ + nb_centers.sort((a, b) => { + return -(a[1] - b[1]) || a[0].localeCompare(b[0]); + }); + const nb_centers_per_power = nb_centers.map((couple) => (couple[0] + ': ' + couple[1])).join(' '); + const note = game.note; + + /* Adding units, influence and orders. */ + const renderedUnits = []; + const renderedDislodgedUnits = []; + const renderedOrders = []; + const renderedOrders2 = []; + const renderedHighestOrders = []; + for (let power of Object.values(game.powers)) if (!power.isEliminated()) { + for (let unit of power.units) { + renderedUnits.push( + <Unit key={unit} + unit={unit} + powerName={power.name} + isDislodged={false} + coordinates={Coordinates} + symbolSizes={SymbolSizes}/> + ); + } + for (let unit of Object.keys(power.retreats)) { + renderedDislodgedUnits.push( + <Unit key={unit} + unit={unit} + powerName={power.name} + isDislodged={true} + coordinates={Coordinates} + symbolSizes={SymbolSizes}/> + ); + } + for (let center of power.centers) { + setInfluence(classes, mapData, center, power.name); + } + for (let loc of power.influence) { + if (!mapData.supplyCenters.has(loc)) + setInfluence(classes, mapData, loc, power.name); + } + + if (orders) { + const powerOrders = (orders && orders.hasOwnProperty(power.name) && orders[power.name]) || []; + for (let order of powerOrders) { + const tokens = order.split(/ +/); + if (!tokens || tokens.length < 3) + continue; + const unit_loc = tokens[1]; + if (tokens[2] === 'H') { + renderedOrders.push( + <Hold key={order} + loc={unit_loc} + powerName={power.name} + coordinates={Coordinates} + symbolSizes={SymbolSizes} + colors={Colors}/> + ); + } else if (tokens[2] === '-') { + const destLoc = tokens[tokens.length - (tokens[tokens.length - 1] === 'VIA' ? 2 : 1)]; + renderedOrders.push( + <Move key={order} + srcLoc={unit_loc} + dstLoc={destLoc} + powerName={power.name} + phaseType={game.getPhaseType()} + coordinates={Coordinates} + symbolSizes={SymbolSizes} + colors={Colors}/> + ); + } else if (tokens[2] === 'S') { + const destLoc = tokens[tokens.length - 1]; + if (tokens.includes('-')) { + const srcLoc = tokens[4]; + renderedOrders2.push( + <SupportMove key={order} + loc={unit_loc} + srcLoc={srcLoc} + dstLoc={destLoc} + powerName={power.name} + coordinates={Coordinates} + symbolSizes={SymbolSizes} + colors={Colors}/> + ); + } else { + renderedOrders2.push( + <SupportHold key={order} + loc={unit_loc} + dstLoc={destLoc} + powerName={power.name} + coordinates={Coordinates} + symbolSizes={SymbolSizes} + colors={Colors}/> + ); + } + } else if (tokens[2] === 'C') { + const srcLoc = tokens[4]; + const destLoc = tokens[tokens.length - 1]; + if ((srcLoc !== destLoc) && (tokens.includes('-'))) { + renderedOrders2.push( + <Convoy key={order} + loc={unit_loc} + srcLoc={srcLoc} + dstLoc={destLoc} + powerName={power.name} + coordinates={Coordinates} colors={Colors} + symbolSizes={SymbolSizes}/> + ); + } + } else if (tokens[2] === 'B') { + renderedHighestOrders.push( + <Build key={order} + unitType={tokens[0]} + loc={unit_loc} + powerName={power.name} + coordinates={Coordinates} + symbolSizes={SymbolSizes}/> + ); + } else if (tokens[2] === 'D') { + renderedHighestOrders.push( + <Disband key={order} + loc={unit_loc} + phaseType={game.getPhaseType()} + coordinates={Coordinates} + symbolSizes={SymbolSizes}/> + ); + } else if (tokens[2] === 'R') { + const destLoc = tokens[3]; + renderedOrders.push( + <Move key={order} + srcLoc={unit_loc} + dstLoc={destLoc} + powerName={power.name} + phaseType={game.getPhaseType()} + coordinates={Coordinates} + symbolSizes={SymbolSizes} + colors={Colors}/> + ); + } else { + throw new Error(`Unknown error to render (${order}).`); + } + } + } + } + + if (this.props.orderBuilding && this.props.orderBuilding.path.length) { + const clicked = parseLocation(this.props.orderBuilding.path[0]); + const province = this.props.mapData.getProvince(clicked); + if (!province) + throw new Error(('Unknown clicked province ' + clicked)); + const clickedID = province.getID(classes); + if (!clicked) + throw new Error(`Unknown path (${clickedID}) for province (${clicked}).`); + classes[clickedID] = 'provinceRed'; + const neighbors = this.getNeighbors(); + if (neighbors) { + for (let neighbor of neighbors) { + const neighborProvince = this.props.mapData.getProvince(neighbor); + if (!neighborProvince) + throw new Error('Unknown neighbor province ' + neighbor); + const neighborID = neighborProvince.getID(classes); + if (!neighborID) + throw new Error(`Unknown neoghbor path (${neighborID}) for province (${neighbor}).`); + classes[neighborID] = neighborProvince.isWater() ? 'provinceBlue' : 'provinceGreen'; + } + } + } + + if (this.props.showAbbreviations === false) { + classes['BriefLabelLayer'] = 'visibilityHidden'; + } + return ( %(svg)s ); } } %(classname)s.propTypes = { - onHover: PropTypes.func + game: PropTypes.instanceOf(Game).isRequired, + mapData: PropTypes.instanceOf(MapData).isRequired, + orders: PropTypes.object, + onHover: PropTypes.func, + onError: PropTypes.func.isRequired, + onSelectLocation: PropTypes.func, + onSelectVia: PropTypes.func, + onOrderBuilding: PropTypes.func, + onOrderBuilt: PropTypes.func, + orderBuilding: PropTypes.object, + showAbbreviations: PropTypes.bool }; -""" % { - 'style_content': "import './%s.css';" % class_name if style_lines else '', - 'extra_content': "import {%s} from './%s';" % (extra_class_name, extra_class_name) if extra else '', - 'classname': class_name, - 'classes': json.dumps(id_to_class), - 'svg': '\n'.join(lines), - 'program_name': sys.argv[0], - 'args': args -}) +""" % {'style_content': "import './%s.css';" % class_name if data.style_lines else '', + 'extra_content': 'import {Coordinates, SymbolSizes, Colors} from "./%s";' % + (extra_class_name) if data.extra else '', + 'classname': class_name, + 'classes': to_json_string(data.id_to_class), + 'svg': '\n'.join(lines)}) + + # Adding license and minifying + map_js_code = LICENSE_TEXT \ + + '\n/** Generated with parameters: %s **/\n' % args \ + + minify(map_js_code) \ + + '// eslint-disable-line semi' + + # Writing to disk + with open(output_file_name, 'w') as file: + file.write(map_js_code) if __name__ == '__main__': |