Skip to content

Instantly share code, notes, and snippets.

@cmc333333
Last active August 29, 2015 14:10
Show Gist options
  • Select an option

  • Save cmc333333/e83a36e2accf08d75b61 to your computer and use it in GitHub Desktop.

Select an option

Save cmc333333/e83a36e2accf08d75b61 to your computer and use it in GitHub Desktop.
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