Last active
August 29, 2015 14:10
-
-
Save cmc333333/e83a36e2accf08d75b61 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/.gitignore b/.gitignore | |
| index 0856a67..1c0630d 100644 | |
| --- a/.gitignore | |
| +++ b/.gitignore | |
| @@ -62,6 +62,7 @@ peacecorps/*/static/*/sass/neat | |
| local_settings.py | |
| peacecorps/media | |
| peacecorps/peacecorps/tests/gpg/random_seed | |
| +peacecorps/peacecorps/scripts/svg | |
| *.swp | |
| diff --git a/peacecorps/peacecorps/scripts/README.md b/peacecorps/peacecorps/scripts/README.md | |
| new file mode 100644 | |
| index 0000000..0cca5f5 | |
| --- /dev/null | |
| +++ b/peacecorps/peacecorps/scripts/README.md | |
| @@ -0,0 +1,28 @@ | |
| +# Scripts | |
| + | |
| +## Crop Map | |
| + | |
| +Wikipedia [provides](http://en.wikipedia.org/wiki/File:BlankMap-World6.svg) a | |
| +beautiful SVG map of the world, complete with well-labeled country boundaries. | |
| +This file is quite large, however, and we must manipulate it to highlight and | |
| +zoom to specific countries. Performing this in the browser is workable when | |
| +there is only one map, but when listing many images of countries, the DOM | |
| +quickly exceeds a reasonable amount of memory. This script, then, splits the | |
| +world map into component countries, providing context in the form of | |
| +surrounding countries and highlighting/zooming to the selected country. | |
| + | |
| +To use the script, you must first install a python | |
| +[library](https://github.com/cjlano/svg) from source. Clone that repository | |
| +within this `scripts` directory. Then run the script with the input SVG file | |
| +and the output directory: | |
| + | |
| +```bash | |
| +git clone https://github.com/cjlano/svg.git | |
| +python cropmap.py ../static/peacecorps/img/BlankMap-World6.svg ../static/peacecorps/img/countries/ | |
| +``` | |
| + | |
| +You will see several debug messages which can be safely ignored. Execution | |
| +takes around 5 minutes to run on a decent machine. | |
| + | |
| +TODO: It'd make sense to simplify the shapes/otherwise reduce the file size. | |
| +The script removes unseen countries, but it could definitely be improved. | |
| diff --git a/peacecorps/peacecorps/scripts/cropmap.py b/peacecorps/peacecorps/scripts/cropmap.py | |
| new file mode 100644 | |
| index 0000000..cc6d9e7 | |
| --- /dev/null | |
| +++ b/peacecorps/peacecorps/scripts/cropmap.py | |
| @@ -0,0 +1,152 @@ | |
| +"""Separates an SVG map into files to represent each of its component | |
| +countries, while retaining some context. This script is tied heavily into our | |
| +country map file, but the script could be modified to resize images, provide | |
| +more context, etc.""" | |
| +import copy | |
| +import itertools | |
| +import logging | |
| +import os | |
| +import sys | |
| +import xml.etree.ElementTree as ET | |
| + | |
| +import svg | |
| + | |
| + | |
| +def highlight(doc, el_id): | |
| + """Modify the document to add a highlight class on the country with the | |
| + provided code""" | |
| + parent = doc.find(".//*[@id='%s']" % el_id) | |
| + for el in itertools.chain([parent], parent.iterfind(".//*")): | |
| + if el.get('class'): | |
| + el.set('class', 'world_map-is_selected ' + el.get('class')) | |
| + return doc | |
| + | |
| + | |
| +def zoom_with_context(doc, el_id): | |
| + """Zoom to the given boundary. Adds a margin of 90% the boundary size or | |
| + 20% of the whole map, whichever is smaller""" | |
| + parent = doc.find(".//*[@id='%s']" % el_id) | |
| + boundary = bbox(parent) | |
| + margin = 0.9 | |
| + root = doc.getroot() | |
| + # The SVG file initially contains the whole map | |
| + svgWidth, svgHeight = map(float, root.get('viewBox').split()[-2:]) | |
| + bndWidth = boundary[1].x - boundary[0].x | |
| + bndHeight = boundary[1].y - boundary[0].y | |
| + if bndWidth * margin > svgWidth * 0.1: | |
| + margin = svgWidth * 0.1 / bndWidth | |
| + if bndHeight * margin > svgHeight * 0.1: | |
| + margin = svgHeight * 0.1 / bndHeight | |
| + | |
| + factor = 1 + (2 * margin) | |
| + zoomPercent = factor * bndWidth / svgWidth | |
| + if zoomPercent < 0.05: | |
| + root.set('class', root.get('class') + ' zoom4') | |
| + elif zoomPercent < 0.1: | |
| + root.set('class', root.get('class') + ' zoom3') | |
| + elif zoomPercent < 0.2: | |
| + root.set('class', root.get('class') + ' zoom2') | |
| + else: | |
| + root.set('class', root.get('class') + ' zoom1') | |
| + | |
| + root.set('viewBox', '%d %d %d %d' % ( | |
| + (boundary[0].x - margin*bndWidth), | |
| + (boundary[0].y - margin*bndHeight), | |
| + factor * bndWidth, factor * bndHeight)) | |
| + | |
| + | |
| +def overlaps(left1, top1, right1, bottom1, left2, top2, right2, bottom2): | |
| + """Check for overlaps between the left and right rectangles""" | |
| + return not (left2 > right1 or right2 < left1 | |
| + or top2 > bottom1 or bottom2 < top1) | |
| + | |
| + | |
| +def crop_to(doc, bboxes): | |
| + """Delete any elements that are not in view""" | |
| + root = doc.getroot() | |
| + namespaces = {"svg": "http://www.w3.org/2000/svg"} | |
| + left, top, width, height = map(float, root.get('viewBox').split()) | |
| + right, bottom = left + width, top + height | |
| + # Delete any path not on screen (even if another portion of the country | |
| + # is visible) | |
| + for key, bbox in bboxes.items(): | |
| + if not overlaps(left, top, right, bottom, | |
| + bbox[0].x, bbox[0].y, bbox[1].x, bbox[1].y): | |
| + parent = root.find(".//*[@id='%s']/.." % key) | |
| + parent.remove(parent.find("./*[@id='%s']" % key)) | |
| + # Delete any groups which no longer have children. We run this five times | |
| + # to account for nesting | |
| + for _ in range(5): | |
| + for parent in root.iterfind(".//svg:g/..", namespaces): | |
| + for child in parent.iterfind("svg:g", namespaces): | |
| + if len(child) == 0 or (len(child) == 1 | |
| + and child[0].tag.endswith('title')): | |
| + parent.remove(child) | |
| + | |
| + | |
| +def ids_to_bboxes(root): | |
| + """Run through all paths, generating a mapping between xml id and bounding | |
| + box""" | |
| + namespaces = {"svg": "http://www.w3.org/2000/svg"} | |
| + mapping = {} | |
| + for path in itertools.chain( | |
| + root.iterfind(".//svg:path[@id]", namespaces), | |
| + root.iterfind(".//svg:circle[@id]", namespaces)): | |
| + mapping[path.get('id')] = bbox(path) | |
| + return mapping | |
| + | |
| + | |
| +def bbox(svg_el): | |
| + """Bounding box for this svg element. Accounts for transformations""" | |
| + if svg_el.tag.endswith("path"): | |
| + return svg.Path(svg_el).bbox() | |
| + elif svg_el.tag.endswith("circle"): | |
| + return svg.Circle(svg_el).bbox() | |
| + else: | |
| + group = svg.Group(svg_el) | |
| + group.append(svg_el) | |
| + group.transform() | |
| + return group.bbox() | |
| + | |
| + | |
| +def country_code(xml_el, namespaces): | |
| + """Find the country code for this element""" | |
| + if xml_el.get("class"): | |
| + return xml_el.get("class").split()[-1] | |
| + elif xml_el.find("svg:path", namespaces) is not None: | |
| + return country_code(xml_el.find("svg:path", namespaces), namespaces) | |
| + else: | |
| + return country_code(xml_el.find("svg:g", namespaces), namespaces) | |
| + | |
| + | |
| +def write_file(doc, outputdir, code): | |
| + """Serialize the xml tree and write it to disk""" | |
| + doc.write(os.path.join(outputdir, code + ".svg")) | |
| + | |
| + | |
| +def cropmap(map_path, outputdir): | |
| + """For each country in the map, create a new map file zoomed to that | |
| + highlighted country""" | |
| + namespaces = {"svg": "http://www.w3.org/2000/svg"} | |
| + doc = ET.parse(map_path) | |
| + root = doc.getroot() | |
| + bboxes = ids_to_bboxes(root) | |
| + for xml_el in itertools.chain(root.iterfind("svg:path", namespaces), | |
| + root.iterfind("svg:g", namespaces)): | |
| + if "landxx" in xml_el.get('class', ''): # skip the ocean paths | |
| + code = country_code(xml_el, namespaces) | |
| + copy_doc = copy.deepcopy(doc) | |
| + highlight(copy_doc, xml_el.get('id')) | |
| + zoom_with_context(copy_doc, xml_el.get('id')) | |
| + crop_to(copy_doc, bboxes) | |
| + | |
| + write_file(copy_doc, outputdir, code) | |
| + logging.info("Wrote %s", code) | |
| + | |
| + | |
| +if __name__ == "__main__": | |
| + if len(sys.argv) < 3: | |
| + print("Usage: python cropmap.py /path/to/svg /path/to/outputdir") | |
| + else: | |
| + logging.basicConfig(level=logging.INFO) | |
| + cropmap(sys.argv[1], sys.argv[2]) | |
| diff --git a/peacecorps/peacecorps/static/peacecorps/img/BlankMap-World6.svg b/peacecorps/peacecorps/static/peacecorps/img/BlankMap-World6.svg | |
| index cff5a3b..b9bbb3b 100644 | |
| --- a/peacecorps/peacecorps/static/peacecorps/img/BlankMap-World6.svg | |
| +++ b/peacecorps/peacecorps/static/peacecorps/img/BlankMap-World6.svg | |
| @@ -1,8 +1,6 @@ | |
| -<svg xmlns="http://www.w3.org/2000/svg" | |
| - height="127" id="svg2985" version="1.1" | |
| - class="world_map" | |
| - viewBox="100 50 300 300 " | |
| - width="127"> | |
| +<svg xmlns="http://www.w3.org/2000/svg" class="world_map" height="127" | |
| + id="svg2985" version="1.1" viewBox="82.992 45.607 2528.5721 1428.3294" | |
| + width="127"> | |
| <title>World Map</title> | |
| <defs id="defs2987"/> | |
| <defs> | |
| diff --git a/peacecorps/peacecorps/static/peacecorps/js/country_map.js b/peacecorps/peacecorps/static/peacecorps/js/country_map.js | |
| deleted file mode 100644 | |
| index 680ff8e..0000000 | |
| --- a/peacecorps/peacecorps/static/peacecorps/js/country_map.js | |
| +++ /dev/null | |
| @@ -1,147 +0,0 @@ | |
| -'use strict'; | |
| - | |
| -/* @namespace */ | |
| -var PC = PC || {}; | |
| - | |
| -(function() { | |
| - | |
| - /* | |
| - * Country Map | |
| - * | |
| - * An SVG map which sets a class to style a certain country and then zooms | |
| - * in on it. | |
| - * | |
| - * @constructor | |
| - * @param {SVGObject} map - An svg DOM object to manipulate. | |
| - * @param {string} selectedCountryCode - The country code used for selecting | |
| - * in the map document. | |
| - */ | |
| - PC.CountryMap = function(map, selectedCountryCode) { | |
| - this.map = map; | |
| - this.selectedCountryCode = selectedCountryCode.toLowerCase(); | |
| - }; | |
| - | |
| - /* | |
| - * Initialize the CountryMap by highlighting the country and zooming in | |
| - * to it. | |
| - */ | |
| - PC.CountryMap.prototype.init = function() { | |
| - var selectedCountry = this.map.querySelectorAll( | |
| - '.' + this.selectedCountryCode)[0], | |
| - coords = null; | |
| - if (!selectedCountry) { | |
| - // TODO handle error condition. | |
| - return; | |
| - } | |
| - coords = this.calculateCountryCoords(selectedCountry); | |
| - | |
| - this.highLightCountry(this.selectedCountryCode); | |
| - this.zoomToCountry(coords, selectedCountry.ownerSVGElement.getBBox()); | |
| - }; | |
| - | |
| - /* | |
| - * Account for SVG transformations when determining a bounding box | |
| - */ | |
| - PC.CountryMap.prototype.calculateCountryCoords = function(element) { | |
| - var bbox = element.getBBox(), | |
| - svg = element.ownerSVGElement, | |
| - transform = element.getTransformToElement(svg), | |
| - ul = svg.createSVGPoint(), | |
| - lr = svg.createSVGPoint(); | |
| - ul.x = bbox.x; | |
| - ul.y = bbox.y; | |
| - lr.x = bbox.x + bbox.width; | |
| - lr.y = bbox.y + bbox.height; | |
| - ul = ul.matrixTransform(transform); | |
| - lr = lr.matrixTransform(transform); | |
| - return {x: ul.x, y: ul.y, width: lr.x - ul.x, height: lr.y - ul.y}; | |
| - }; | |
| - | |
| - /* | |
| - * Highlight the country by setting a certain css class on it. This class | |
| - * should be styled externally. | |
| - * | |
| - * @param {string} countryCode - The country code to select and style. | |
| - */ | |
| - PC.CountryMap.prototype.highLightCountry = function(countryCode) { | |
| - var countryPaths = [], | |
| - path = {}, | |
| - i, ilen; | |
| - | |
| - countryPaths = this.map.querySelectorAll('.' + countryCode); | |
| - for (i = 0, ilen = countryPaths.length; i < ilen; i++) { | |
| - path = countryPaths[i]; | |
| - // TODO replace with class adding util. | |
| - if (path.classList) { | |
| - path.classList.add('world_map-is_selected'); | |
| - } else { | |
| - path.className += ' ' + 'world_map-is_selected'; | |
| - } | |
| - } | |
| - }; | |
| - | |
| - /* | |
| - * Zoom to the country. Adds a margin of 80% the country size or 20% of the | |
| - * whole map, whichever is smaller. | |
| - * | |
| - * @param {object} countryCoords - The coordinates to zoom to. | |
| - */ | |
| - PC.CountryMap.prototype.zoomToCountry = function(countryCoords, mapBounds) { | |
| - var margin = 0.9, /* as a percentage of the country size */ | |
| - coords = $.extend({}, countryCoords), | |
| - factor = null, | |
| - imgWidth = null, | |
| - imgHeight = null, | |
| - zoomLevel = null; | |
| - | |
| - if (coords.width * margin > mapBounds.width * 0.1) { | |
| - margin = mapBounds.width * 0.1 / coords.width; | |
| - } | |
| - if (coords.height * margin > mapBounds.height * 0.1) { | |
| - margin = mapBounds.height * 0.1 / coords.height; | |
| - } | |
| - | |
| - factor = 1 + (2 * margin); | |
| - imgWidth = factor * coords.width; | |
| - imgHeight = factor * coords.height; | |
| - if (imgWidth / mapBounds.width < 0.05) { zoomLevel = 'zoom4'; } | |
| - else if (imgWidth / mapBounds.width < 0.1) { zoomLevel = 'zoom3'; } | |
| - else if (imgWidth / mapBounds.width < 0.2) { zoomLevel = 'zoom2'; } | |
| - else { zoomLevel = 'zoom1'; } | |
| - // TODO replace with class adding util. | |
| - if (this.map.classList) { | |
| - this.map.classList.add(zoomLevel); | |
| - } else { | |
| - this.map.className += ' ' + zoomLevel; | |
| - } | |
| - | |
| - this.map.setAttribute('viewBox', | |
| - (coords.x - margin*coords.width) + ' ' + | |
| - (coords.y - margin*coords.height) + ' ' + | |
| - imgWidth + ' ' + imgHeight); | |
| - | |
| - }; | |
| - | |
| - // main - if necessary, fetch the map and update countries | |
| - $(document).ready(function() { | |
| - var $mapEls = $('.map'); | |
| - if ($mapEls) { | |
| - // Grab the data only once; assume they all have the same url; | |
| - // note that jQuery adds the required "Origin" header (for CORS) | |
| - $.ajax({ | |
| - url: $mapEls.data('url'), | |
| - dataType: 'html' | |
| - }).done(function (svgEl) { | |
| - // Then run the map manipulation for each svg | |
| - $mapEls.each(function(idx, mapEl) { | |
| - var $mapEl = $(mapEl), | |
| - countryMap; | |
| - $mapEl.append(svgEl); | |
| - countryMap = new PC.CountryMap($mapEl.children()[0], | |
| - $mapEl.data('country')); | |
| - countryMap.init(); | |
| - }); | |
| - }); | |
| - } | |
| - }); | |
| -})(); | |
| diff --git a/peacecorps/peacecorps/templates/donations/donate_all.jinja b/peacecorps/peacecorps/templates/donations/donate_all.jinja | |
| index d29d0f2..921afbb 100644 | |
| --- a/peacecorps/peacecorps/templates/donations/donate_all.jinja | |
| +++ b/peacecorps/peacecorps/templates/donations/donate_all.jinja | |
| @@ -88,15 +88,11 @@ | |
| {% endblock %} | |
| -{% block custom_js %} | |
| -<script type="text/javascript" src="{{ static("peacecorps/js/country_map.js") | |
| - }}"></script> | |
| -{% endblock %} | |
| - | |
| {% macro country_map(code) -%} | |
| - <div class="mask mask--oval mask--bordered u-h_center--block map" | |
| - data-country="{{code}}" | |
| - data-url="{{ static("peacecorps/img/blankmap-world6.svg") }}"> | |
| + <div class="mask mask--oval mask--bordered u-h_center--block map"> | |
| + <img src="{{ static("peacecorps/img/countries/" + code.lower() | |
| + + ".svg") }}" | |
| + class="mask__content" /> | |
| </div> | |
| {%- endmacro %} | |
| diff --git a/peacecorps/peacecorps/templates/donations/donate_project.jinja b/peacecorps/peacecorps/templates/donations/donate_project.jinja | |
| index 7653391..5b7b60c 100644 | |
| --- a/peacecorps/peacecorps/templates/donations/donate_project.jinja | |
| +++ b/peacecorps/peacecorps/templates/donations/donate_project.jinja | |
| @@ -114,7 +114,5 @@ | |
| {% block custom_js %} | |
| <script type="text/javascript" src="{{ static("peacecorps/js/update_donatepercent.js") | |
| }}"></script> | |
| -<script type="text/javascript" src="{{ static("peacecorps/js/country_map.js") | |
| - }}"></script> | |
| {% endblock %} | |
| diff --git a/peacecorps/peacecorps/templates/donations/includes/project-top.jinja b/peacecorps/peacecorps/templates/donations/includes/project-top.jinja | |
| index ed1b3b5..ba9ab70 100644 | |
| --- a/peacecorps/peacecorps/templates/donations/includes/project-top.jinja | |
| +++ b/peacecorps/peacecorps/templates/donations/includes/project-top.jinja | |
| @@ -32,9 +32,10 @@ | |
| </span> | |
| </section> | |
| <section class="showcase showcase--col3_centered__col"> | |
| - <div class="mask mask--oval mask--bordered u-h_center--block map" | |
| - data-country="{{project.country.code}}" | |
| - data-url="{{ static("peacecorps/img/BlankMap-World6.svg") }}"> | |
| + <div class="mask mask--oval mask--bordered u-h_center--block map"> | |
| + <img src="{{ static("peacecorps/img/countries/" | |
| + + project.country.code.lower() + ".svg") }}" | |
| + class="mask__content" /> | |
| </div> | |
| <span class="t-body--sm"> | |
| in <strong>{{project.country.name}}</strong> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment