Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit eae89568805861898f2aff092e77e58043143829 0 parents
@m2w authored
Showing with 1,654 additions and 0 deletions.
  1. +5 −0 .gitignore
  2. +4 −0 LICENSE
  3. +25 −0 Makefile
  4. +54 −0 README.md
  5. +11 −0 priv/dispatch.conf
  6. +46 −0 priv/scripts/parse_eval.py
  7. +34 −0 priv/static/erli.css
  8. +144 −0 priv/static/erli.js
  9. +146 −0 priv/static/worldmap.js
  10. +2 −0  python.requirements
  11. +8 −0 rebar.config
  12. +26 −0 src/erli.app.src
  13. +54 −0 src/erli.erl
  14. +15 −0 src/erli.hrl
  15. +21 −0 src/erli_app.erl
  16. +71 −0 src/erli_error_handler.erl
  17. +85 −0 src/erli_stats.erl
  18. +278 −0 src/erli_storage.erl
  19. +60 −0 src/erli_sup.erl
  20. +23 −0 src/erli_util.erl
  21. +150 −0 src/path_resource.erl
  22. +88 −0 src/root_resource.erl
  23. +77 −0 src/static_resource.erl
  24. +3 −0  start.sh
  25. +26 −0 templates/base.dtl
  26. +9 −0 templates/error_handlers/badgateway.dtl
  27. +9 −0 templates/error_handlers/badrequest.dtl
  28. +8 −0 templates/error_handlers/forbidden.dtl
  29. +9 −0 templates/error_handlers/notfound.dtl
  30. +9 −0 templates/error_handlers/notimplemented.dtl
  31. +9 −0 templates/error_handlers/serverfault.dtl
  32. +9 −0 templates/error_handlers/unavailable.dtl
  33. +81 −0 templates/index.dtl
  34. +11 −0 templates/landing.dtl
  35. +11 −0 templates/report.dtl
  36. +33 −0 templates/stats.dtl
5 .gitignore
@@ -0,0 +1,5 @@
+deps/*
+ebin/*
+priv/log/*
+priv/mnesia.store/*
+.eunit/*
4 LICENSE
@@ -0,0 +1,4 @@
+Copyright (c) 2012 Moritz Windelen
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 Makefile
@@ -0,0 +1,25 @@
+ERL ?= erl
+APP := erli
+
+.PHONY: deps
+
+all: deps
+ @./rebar compile
+
+deps:
+ @./rebar get-deps
+
+clean:
+ @./rebar clean
+
+distclean: clean
+ @./rebar delete-deps
+
+docs:
+ @erl -noshell -run edoc_run application '$(APP)' '"."' '[]'
+
+test:
+ @./rebar skip_deps=true eunit
+
+compile:
+ @./rebar compile
54 README.md
@@ -0,0 +1,54 @@
+# erli
+
+**erli** is an Erlang based URL-shortener, with statistics and 'nsfw' functionality.
+
+## statistics
+
+Every shortened URL keeps track of visitor counts (non-real time total and unique visit counts) and provides estimates for the geographic location of visitors.
+
+## 'nsfw' functionality
+
+Appending `?landing=true` to a shortened URL, prevents the automatic redirect, instead a landing page, showing the full target URL and an "are you sure you want to proceed" button, is displayed.
+
+Reporting inappropriate URLs is kept extremely simple, just add `/report` to the URL. URLs that reach a certain threshold - this mechanism will be expanded in the future (pull requests are most welcome ;)) - are permanetly banned.
+
+## demo
+
+Live demo will be up as soon as I get around to it.
+
+To play around with it on localhost, just clone, `make compile`, `./start.sh` and open [http://localhost:8000](http://localhost:8000).
+
+## todo
+
+security:
+
++ implement throttling via 503 with `retry-after` (based on a simple request/interval scheme?)
+
+usability:
+
++ timeline of visits by hour
++ thumbnails on the landing pages?
++ ensure that no matter the `STATS_COLLECT_INTERVAL`, statistics don't get corrupted
+
+code quality:
+
++ eunit tests
+
+## the api
+
+### root_resource.erl
+
+ GET / -> displays the index
+ POST / -> create a new shortened URL with a generated path
+
+### path_resource.erl
+
+ GET /path -> grab the redirect to the target URL
+ DELETE /path -> report the target
+ PUT /path -> create a new shortened url with a preferred path
+ -----------------
+ GET /path/report -> report the target URL
+ -----------------
+ GET /path/stats -> view stats for the path
+ -----------------
+ GET /path/check -> utility URL to facility 'low' overhead checking whether a path is already taken via ajax (@ W3C please give us an option to disable the automatic redirect on 30x!)
11 priv/dispatch.conf
@@ -0,0 +1,11 @@
+%%-*- mode: erlang -*-
+{[], root_resource, []}.
+{["static", '*'], static_resource, ["static"]}.
+{[path, '*'], path_resource, []}.
+
+
+
+
+
+
+
46 priv/scripts/parse_eval.py
@@ -0,0 +1,46 @@
+import datetime
+import os
+import subprocess
+
+from erlport import Atom, Port, Protocol, String
+import pygeoip
+
+
+class ParseEval(Protocol):
+ """Parses the webmachine access log for usage statistics of shortened URLs"""
+
+ def __init__(self, log_dir=None):
+ super(ParseEval, self).__init__()
+ self.log_dir = log_dir or os.path.join(os.path.abspath(os.getcwd()),
+ 'priv',
+ 'log')
+
+ def handle_path(self, path):
+ # parse the log files from the last hour
+ log_timestamp = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
+ log_fname = 'access.log.%d_%02d_%02d_%02d' % (log_timestamp.year,
+ log_timestamp.month,
+ log_timestamp.day,
+ log_timestamp.hour)
+ log_fpath = os.path.join(self.log_dir, log_fname)
+ if not os.path.exists(log_fpath):
+ return Atom('reschedule'), log_timestamp.minute + 1
+
+ parse_cmd = 'grep -e \'GET /%s \' %s | awk \'{ print $1 }\'' % (String(path),
+ log_fpath)
+ gi = pygeoip.GeoIP('/usr/local/GeoIP/GeoIP.dat')
+ proc = subprocess.Popen(parse_cmd, shell=True, stdout=subprocess.PIPE)
+ stdout, stderr = proc.communicate()
+ total_visits = 0
+ countries = set()
+ ips = set()
+ for ip in stdout.splitlines():
+ countries.add(gi.country_code_by_addr(ip))
+ ips.add(ip)
+ total_visits += 1
+ return list(countries), list(ips), total_visits
+
+
+if __name__ == "__main__":
+ pe = ParseEval()
+ pe.run(Port(use_stdio=True))
34 priv/static/erli.css
@@ -0,0 +1,34 @@
+h1 a:link {
+ text-decoration: none;
+ color: #404040;
+}
+h1 a:visited {
+ text-decoration: none;
+ color: #404040;
+}
+h1 a:hover {
+ text-decoration: none;
+ color: #404040;
+}
+body {
+ text-align: center;
+ padding-top: 40px;
+}
+label {
+ float: none;
+}
+.container {
+ width: 820px;
+}
+.alert-message {
+ margin-bottom: 0px;
+}
+form .input {
+ margin-left: 0px;
+}
+form .clearfix.error input, form .clearfix.error textarea {
+ color: gray;
+}
+#about_popover, #url_structure_popup, #path_help_popup {
+ color: #BFBFBF;
+}
144 priv/static/erli.js
@@ -0,0 +1,144 @@
+/* global vars */
+var re = /^(?:[a-zA-Z0-9]+:\/\/)(?:(?:(?:[a-zA-Z0-9]+\.)+(?:[a-zA-Z]+))|(?:(?:[0-9]+\.){3}(?:[0-9]+))|(?:(?:[a-f0-9]+\:)+(?:[a-f0-9]+)))(?:(?:\s*$)|(?:(?:[:\/?]).+$))/;
+
+/* utility functions */
+function show_error_and_helptext(E) {
+ E.parent().addClass("error");
+ E.siblings('.help-inline').show();
+}
+
+function hide_error_and_helptext(E) {
+ E.parent().removeClass("error");
+ E.siblings('.help-inline').hide();
+}
+
+$(document).ready(function() {
+ $(".alert-message.warning").remove(); // close the banner for non-js enabled visitors
+ $('.alert-message.error').alert();
+ $('#url_structure_popup').popover();
+ $('#path_help_popup').popover();
+
+ /* Modal ToU button click handlers */
+ $(".modal-footer a.primary").click(function(e){
+ $("#tou_checkbox").attr("checked", "checked");
+ $("#modal-tou").modal("hide");
+ e.preventDefault();
+ });
+ $(".modal-footer a.secondary").click(function(e){
+ $("#modal-tou").modal("hide");
+ e.preventDefault();
+ });
+
+ /* validation functions for the URL submission form */
+ $('#tou_checkbox').change(function(e){
+ var parDiv = $(this).parents('div.clearfix.input');
+ parDiv.removeClass("error");
+ });
+ $('#url_input').keyup(function(){
+ hide_error_and_helptext($(this));
+ });
+ $('#pref_path_input').keyup(function() {
+ $.ajax({
+ type: 'GET',
+ url: $(this).val() + '/check', // need to append the check call to guarantee that the status code is either 200 or 404
+ statusCode:
+ {
+ 404: function() {
+ // path available
+ hide_error_and_helptext($('#pref_path_input'));
+ },
+ 200: function() {
+ // path taken
+ show_error_and_helptext($('#pref_path_input'));
+ }
+ }
+ });
+ });
+
+ $('#url_input').blur(function(){
+ if (!$('#url_input').val().match(re)){
+ show_error_and_helptext($('#url_input'));
+ }
+ });
+
+ /* form submission */
+ $("#shorten_url_form").submit(function() {
+ var url = $("#url_input").val();
+ var checked = $("#tou_checkbox").attr("checked") ? true : false;
+ var pref_path = $('#pref_path_input').val();
+ if (!checked || !url) {
+ if (!url) {
+ show_error_and_helptext($('#url_input'));
+ }
+ if (!checked) {
+ $('#tou_checkbox').parents('div.clearfix.input').addClass("error");
+ }
+ return false;
+ }
+
+ if (!url.match(re)){
+ show_error_and_helptext($('#url_input'));
+ return false;
+ }
+ if (pref_path) {
+ $.ajax({
+ type: 'PUT',
+ url: pref_path,
+ contentType: 'application/json',
+ data: JSON.stringify({
+ url: url,
+ tou_checked: checked
+ }),
+ statusCode: {
+ 204: function(){
+ $('#path').attr('href', pref_path).text(pref_path);
+ $('#path_stats').attr('href', pref_path + '/stats').text(pref_path + '/stats');
+ $('#success_banner').show();
+ },
+ 400: function(){
+ // bad request - should never occur
+ },
+ 409: function(){
+ // conflict, pref_path is already taken
+ show_error_and_helptext($('#pref_path_input'));
+ },
+ 410: function(){
+ // target url is permanently banned
+ $('#banned_url').text($('#url_input').val());
+ $('.alert-message.error').show();
+ }
+ }
+ });
+ }
+ else {
+ $.ajax({
+ type:'POST',
+ url:'/',
+ data: JSON.stringify({
+ url: url,
+ tou_checked: checked
+ }),
+ contentType: 'application/json',
+ dataType: 'json',
+ processData: false, // we want the raw json object to be transmitted
+ statusCode: {
+ 201: function(data, textStatus, jqXHR) {
+ var location = jqXHR.getResponseHeader('Location');
+ $('#path').attr('href', location).text(location);
+ $('#path_stats').attr('href', location + '/stats').text(location + '/stats');
+ $('#success_banner').show();
+ },
+ 400: function(){
+ },
+ 410: function(){
+ $('#banned_url').text($('#url_input').val());
+ $('.alert-message.error').show();
+ },
+ 500: function(){
+ }
+ }
+ });
+ }
+ return false;
+ });
+});
146 priv/static/worldmap.js
@@ -0,0 +1,146 @@
+/*
+ * Canvas World Map function (documentation: http://joncom.be/code/excanvas-world-map)
+ *
+ * Copyright (c) 2009 Jon Combe (http://joncom.be)
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+function WorldMap(oSettings) {
+ // create variables
+ var oSettings = (oSettings || {});
+ oSettings.detail = (oSettings.detail || {});
+ var sBGColor = (oSettings.bgcolor || "#ffffff");
+ var sFGColor = (oSettings.fgcolor || "#dddddd");
+ var sBorderColor = (oSettings.bordercolor || "#aaaaaa");
+ var iPadding = ((oSettings.padding || 10) * 2);
+ var sZoom = (oSettings.zoom || "ca,cl,us,ru");
+ var iOffsetX = 0;
+ var iOffsetY = 0;
+
+ // get canvas dimensions, set bgcolor
+ var oCanvas = document.getElementById(oSettings.id);
+ if (!oCanvas) {
+ alert("Error: missing or incorrect canvas 'id'");
+ }
+ var iCanvasWidth = oCanvas.width;
+ var iCanvasHeight = oCanvas.height;
+ oCanvas.style.backgroundColor = sBGColor;
+
+ // create drawing area
+ var oCTX = (oCanvas).getContext('2d');
+ oCTX.clearRect(0, 0, iCanvasWidth, iCanvasHeight);
+ oCTX.lineWidth = (oSettings.borderwidth || 1);
+
+ // calculate zoom: create variables
+ var aZoom = sZoom.split(",");
+ var iMinX = oWorldMap[aZoom[0]][0][0][0];
+ var iMaxX = oWorldMap[aZoom[0]][0][0][0];
+ var iMinY = oWorldMap[aZoom[0]][0][0][1];
+ var iMaxY = oWorldMap[aZoom[0]][0][0][1];
+
+ // calculate zoom: find map range
+ for (var iCountry = 0; iCountry < aZoom.length; iCountry++) {
+ for (var iPath = 0; iPath < oWorldMap[aZoom[iCountry]].length; iPath++) {
+ for (iCoord = 0; iCoord < oWorldMap[aZoom[iCountry]][iPath].length; iCoord++) {
+ iMinX = Math.min( iMinX, oWorldMap[aZoom[iCountry]][iPath][iCoord][0] );
+ iMaxX = Math.max( iMaxX, oWorldMap[aZoom[iCountry]][iPath][iCoord][0] );
+ iMinY = Math.min( iMinY, oWorldMap[aZoom[iCountry]][iPath][iCoord][1] );
+ iMaxY = Math.max( iMaxY, oWorldMap[aZoom[iCountry]][iPath][iCoord][1] );
+ }
+ }
+ }
+
+ // calculate zoom ratio
+ var iRatio = Math.min( ((iCanvasWidth - iPadding) / (iMaxX - iMinX)), ((iCanvasHeight - iPadding) / (iMaxY - iMinY)) );
+
+ // calculate zoom offsets
+ var iMidX = (iMinX + ((iMaxX - iMinX) / 2));
+ var iMidY = (iMinY + ((iMaxY - iMinY) / 2));
+ iOffsetX = ((iMidX * iRatio) - (iCanvasWidth / 2));
+ iOffsetY = ((iMidY * iRatio) - (iCanvasHeight / 2));
+
+ // draw "plain" countries
+ for (var sCountry in oWorldMap) {
+ Draw(sCountry, sFGColor);
+ }
+
+ // draw "details" countries
+ for (var sCountry in oSettings.detail) {
+ if (oWorldMap[sCountry]) {
+ Draw(sCountry, oSettings.detail[sCountry]);
+ }
+ }
+
+ // private draw function
+ function Draw(sCountry, sColor) {
+ oCTX.fillStyle = sColor;
+ oCTX.strokeStyle = sBorderColor;
+ oCTX.beginPath();
+
+ // loop through paths
+ var bIE = (navigator.userAgent.indexOf("MSIE") > -1);
+ for (var iPath = 0; iPath < oWorldMap[sCountry].length; iPath++) {
+ oCTX.moveTo((oWorldMap[sCountry][iPath][0][0] * iRatio) - iOffsetX, (oWorldMap[sCountry][iPath][0][1] * iRatio) - iOffsetY);
+ for (iCoord = 1; iCoord < oWorldMap[sCountry][iPath].length; iCoord++) {
+ oCTX.lineTo((oWorldMap[sCountry][iPath][iCoord][0] * iRatio) - iOffsetX, (oWorldMap[sCountry][iPath][iCoord][1] * iRatio) - iOffsetY);
+ }
+ oCTX.closePath();
+ oCTX.fill();
+
+ // IE, again...
+ if (bIE == true) {
+ oCTX.beginPath();
+ oCTX.moveTo((oWorldMap[sCountry][iPath][0][0] * iRatio) - iOffsetX, (oWorldMap[sCountry][iPath][0][1] * iRatio) - iOffsetY);
+ for (iCoord = 1; iCoord < oWorldMap[sCountry][iPath].length; iCoord++) {
+ oCTX.lineTo((oWorldMap[sCountry][iPath][iCoord][0] * iRatio) - iOffsetX, (oWorldMap[sCountry][iPath][iCoord][1] * iRatio) - iOffsetY);
+ }
+ oCTX.closePath();
+ }
+ oCTX.stroke();
+ }
+
+ // awful hack for Lesotho / South Africa (draw Lesotho again, kids!)
+ if (sCountry == "za") {
+ // choose colour
+ if (oSettings.detail["ls"]) {
+ oCTX.fillStyle = oSettings.detail["ls"];
+ } else {
+ oCTX.fillStyle = sFGColor;
+ }
+
+ // loop through paths
+ oCTX.beginPath();
+ for (var iPath = 0; iPath < oWorldMap["ls"].length; iPath++) {
+ oCTX.moveTo((oWorldMap["ls"][iPath][0][0] * iRatio) - iOffsetX, (oWorldMap["ls"][iPath][0][1] * iRatio) - iOffsetY);
+ for (iCoord = 1; iCoord < oWorldMap["ls"][iPath].length; iCoord++) {
+ oCTX.lineTo((oWorldMap["ls"][iPath][iCoord][0] * iRatio) - iOffsetX, (oWorldMap["ls"][iPath][iCoord][1] * iRatio) - iOffsetY);
+ }
+ oCTX.closePath();
+ oCTX.fill();
+ oCTX.stroke();
+ }
+ }
+ }
+}
+
+var oWorldMap={"ad":[[[4687,2398],[4679,2402],[4679,2398]]],"ae":[[[5995,2942],[6059,2942],[6115,2890],[6119,2902],[6119,2922],[6107,2922],[6107,2946],[6099,2946],[6087,2986],[6023,2978]]],"af":[[[6239,2782],[6263,2754],[6263,2738],[6239,2734],[6231,2686],[6251,2614],[6283,2626],[6367,2554],[6387,2562],[6419,2566],[6459,2566],[6499,2526],[6511,2530],[6511,2542],[6519,2542],[6515,2566],[6523,2582],[6555,2562],[6591,2558],[6607,2566],[6599,2570],[6583,2578],[6555,2574],[6523,2586],[6511,2598],[6523,2626],[6503,2646],[6507,2654],[6499,2662],[6475,2662],[6487,2682],[6467,2690],[6459,2722],[6447,2730],[6431,2726],[6399,2742],[6383,2750],[6383,2778],[6323,2794],[6279,2794]]],"ag":[[[3015.38,3131.88],[3013.25,3136.25],[3020,3136.12]],[[3014.62,3116.75],[3016.75,3122.25],[3018.5,3120.25]]],"ai":[[[2983.25,3102],[2980.62,3105.88],[2986,3102.5]],[[2984.62,3105.75],[2980.12,3108.5],[2984.75,3110]]],"al":[[[5167,2398],[5183,2418],[5179,2442],[5191,2454],[5191,2462],[5171,2494],[5151,2470],[5155,2422],[5147,2422]]],"am":[[[5823,2438],[5783,2442],[5787,2478],[5811,2486],[5843,2494],[5851,2514],[5859,2514],[5859,2490],[5839,2482],[5847,2470]]],"an":[[[2822.83,3260.33],[2834.17,3268.58],[2831.67,3269.92]],[[2843.92,3262],[2849.08,3265.83],[2846.42,3270.17]]],"ao":[[[4955,3722],[4975,3702],[4983,3710],[4967,3722],[4967,3738],[4959,3738]],[[4967,3746],[4979,3742],[5075,3742],[5099,3798],[5147,3798],[5151,3770],[5179,3770],[5179,3778],[5211,3778],[5223,3886],[5271,3878],[5271,3930],[5215,3930],[5215,4014],[5255,4058],[5187,4066],[5139,4062],[5123,4050],[5007,4050],[4983,4038],[4947,4046],[4947,4006],[4971,3942],[4999,3910],[5003,3878],[4979,3826],[4991,3814],[4963,3750]]],"ar":[[[2875,4198],[2903,4170],[2911,4178],[2943,4182],[2951,4198],[2959,4178],[2995,4182],[3047,4226],[3127,4270],[3099,4322],[3175,4326],[3199,4306],[3207,4274],[3227,4274],[3227,4318],[3127,4406],[3103,4514],[3103,4534],[3139,4558],[3155,4602],[3127,4642],[3003,4662],[3003,4722],[2935,4722],[2943,4770],[2915,4854],[2867,4878],[2867,4910],[2911,4926],[2907,4958],[2863,4998],[2859,5022],[2827,5038],[2823,5058],[2843,5106],[2803,5094],[2751,5094],[2739,5046],[2719,5050],[2711,5002],[2763,4866],[2747,4766],[2763,4666],[2779,4650],[2799,4494],[2787,4434],[2839,4306],[2839,4250],[2871,4230]]],"as":[[[9613,3964.17],[9607.5,3966.17],[9609.67,3968]]],"at":[[[4895,2238],[4979,2238],[4975,2218],[5003,2198],[5027,2202],[5035,2186],[5087,2198],[5087,2230],[5067,2258],[5023,2274],[4999,2270],[4959,2254],[4915,2262],[4891,2250]]],"au":[[[8331,4062],[8375,3874],[8423,3970],[8455,3986],[8483,4086],[8659,4274],[8671,4366],[8639,4474],[8583,4566],[8575,4622],[8483,4666],[8439,4642],[8411,4666],[8311,4622],[8291,4562],[8251,4518],[8207,4542],[8147,4458],[8071,4446],[7951,4466],[7887,4514],[7791,4514],[7735,4546],[7663,4518],[7679,4462],[7623,4306],[7639,4278],[7615,4230],[7707,4138],[7823,4110],[7867,4030],[7887,4046],[7975,3954],[8007,3986],[8043,3986],[8039,3966],[8083,3906],[8119,3910],[8131,3886],[8235,3918],[8199,3982]],[[8439,4718],[8487,4734],[8535,4718],[8535,4762],[8499,4810],[8475,4806],[8439,4730]]],"aw":[[[2799.58,3254.08],[2804.92,3259.75],[2800.08,3258.42]]],"ax":[[[5167.55,1771.71],[5152.61,1777.71],[5171.63,1788.18]]],"az":[[[5859,2418],[5855,2426],[5867,2438],[5863,2446],[5831,2434],[5823,2438],[5847,2470],[5839,2482],[5859,2490],[5859,2514],[5899,2490],[5923,2530],[5939,2474],[5959,2466],[5939,2458],[5915,2422],[5899,2442]],[[5843,2494],[5811,2486],[5835,2510],[5851,2514]]],"ba":[[[5143,2322],[5143,2370],[5123,2402],[5051,2326],[5055,2314]]],"bb":[[[3073.33,3235.62],[3079.38,3240.04],[3074.21,3243.46]]],"bd":[[[6979,3006],[6971,2938],[6951,2930],[6975,2910],[6955,2894],[6963,2878],[6995,2886],[6999,2910],[7067,2918],[7035,2954],[7043,2978],[7063,2954],[7071,3002],[7071,3022],[7059,3030],[7043,2982]]],"be":[[[4707,2118],[4727,2106],[4771,2102],[4795,2114],[4787,2126],[4799,2126],[4807,2142],[4799,2150],[4791,2158],[4791,2170],[4783,2170]]],"bf":[[[4663,3298],[4703,3274],[4645,3194],[4615,3194],[4587,3206],[4583,3214],[4563,3218],[4499,3278],[4495,3314],[4527,3334],[4547,3322],[4567,3342],[4567,3298],[4627,3298],[4635,3294],[4643,3294]]],"bg":[[[5243,2438],[5239,2418],[5227,2406],[5243,2378],[5227,2358],[5235,2346],[5243,2358],[5315,2362],[5355,2350],[5391,2362],[5387,2374],[5359,2402],[5375,2418],[5331,2426],[5327,2438],[5303,2442],[5283,2430]]],"bh":[[[5962.25,2884.67],[5970.12,2881.33],[5967.79,2894.33]]],"bi":[[[5447,3650],[5451,3674],[5419,3706],[5407,3678],[5399,3662]]],"bj":[[[4711,3418],[4711,3350],[4719,3350],[4739,3314],[4731,3286],[4735,3278],[4711,3258],[4703,3274],[4663,3298],[4659,3314],[4679,3326],[4687,3422]]],"bn":[[[7663,3458],[7671,3474],[7655,3462],[7655,3478],[7651,3482],[7639,3470]]],"bo":[[[2815,4050],[2827,4030],[2839,4018],[2819,3998],[2835,3918],[2815,3878],[2847,3878],[2887,3850],[2923,3842],[2931,3906],[3055,3954],[3059,4018],[3107,4018],[3107,4046],[3127,4070],[3115,4122],[3087,4102],[3019,4110],[2995,4182],[2959,4178],[2951,4198],[2943,4182],[2911,4178],[2903,4170],[2875,4198],[2855,4198]]],"br":[[[3283,3478],[3247,3530],[3207,3526],[3155,3538],[3095,3554],[3071,3538],[3063,3454],[3047,3450],[3047,3458],[2987,3494],[2939,3478],[2959,3522],[2975,3526],[2907,3570],[2883,3558],[2867,3534],[2855,3542],[2803,3542],[2803,3558],[2819,3558],[2819,3570],[2799,3570],[2799,3594],[2819,3622],[2803,3698],[2779,3698],[2727,3722],[2719,3758],[2695,3786],[2747,3854],[2787,3838],[2787,3878],[2847,3878],[2887,3850],[2923,3842],[2931,3906],[3055,3954],[3059,4018],[3107,4018],[3107,4046],[3127,4070],[3115,4122],[3115,4178],[3175,4182],[3187,4230],[3215,4230],[3207,4274],[3227,4274],[3227,4318],[3127,4406],[3147,4402],[3243,4478],[3231,4494],[3235,4510],[3359,4358],[3363,4278],[3435,4222],[3539,4198],[3611,4054],[3623,3934],[3719,3818],[3723,3770],[3707,3722],[3663,3718],[3587,3662],[3491,3650],[3471,3662],[3455,3626],[3383,3602],[3355,3626],[3367,3594],[3323,3578],[3327,3542],[3315,3538]]],"bs":[[[2587,2914],[2579,2930],[2591,2938],[2599,2930]],[[2599,2938],[2591,2942],[2603,2954]],[[2571,2874],[2595,2866],[2595,2870]],[[2607,2866],[2619,2878],[2611,2890],[2615,2878]]],"bt":[[[6971,2862],[6999,2870],[7059,2866],[7055,2850],[7047,2850],[7047,2842],[7003,2826]]],"bw":[[[5299,4058],[5375,4162],[5411,4178],[5347,4222],[5311,4278],[5247,4266],[5211,4310],[5183,4310],[5179,4298],[5187,4290],[5163,4254],[5163,4174],[5191,4174],[5191,4074],[5251,4066],[5259,4078]]],"by":[[[5379,1934],[5339,1954],[5311,2006],[5259,2018],[5267,2062],[5251,2078],[5259,2082],[5259,2098],[5303,2086],[5443,2106],[5455,2082],[5475,2082],[5467,2042],[5499,2034],[5451,1986],[5451,1954]]],"bz":[[[2299,3114],[2319,3094],[2319,3146],[2303,3166],[2299,3166]]],"ca":[[[3159,2102],[3083,2230],[3247,2250],[3227,2170],[3155,2162],[3187,2094]],[[2583,1346],[2723,1446],[2739,1486],[2683,1534],[2703,1558],[2611,1570],[2591,1602],[2623,1614],[2691,1602],[2771,1666],[2907,1706],[2835,1638],[2931,1670],[2951,1642],[2859,1526],[2971,1582],[3027,1510],[2855,1422],[2883,1386],[2835,1322],[2603,1226],[2539,1246],[2495,1174],[2395,1206],[2395,1270],[2367,1226],[2407,1174],[2335,1174],[2275,1254],[2279,1294],[2339,1310],[2291,1310],[2339,1350],[2571,1362]],[[2519,1178],[2555,1222],[2643,1218],[2595,1182]],[[1899,1190],[1871,1222],[1895,1306],[1991,1362],[1959,1406],[1835,1386],[1667,1430],[1559,1358],[1695,1338],[1555,1330],[1527,1310],[1583,1286],[1515,1278],[1535,1234],[1623,1190],[1651,1226],[1667,1210],[1727,1238],[1731,1214],[1783,1234],[1803,1278],[1803,1198],[1855,1210],[1835,1182],[1871,1174]],[[1611,1186],[1479,1254],[1471,1286],[1411,1314],[1335,1262],[1387,1170],[1363,1146],[1455,1134],[1551,1150]],[[1587,970],[1415,1042],[1559,1038]],[[1615,1030],[1559,1098],[1707,1142],[1859,1098],[1863,1054],[1799,1046],[1775,1006],[1743,1078]],[[1871,870],[1979,890],[2027,954],[1907,930]],[[1903,1022],[2075,1034],[2067,1106],[2015,1106]],[[2135,1082],[2103,1106],[2167,1126],[2183,1094]],[[2091,1002],[2259,1014],[2299,1074],[2535,1078],[2523,1130],[2279,1138],[2211,1090],[2187,1034],[2147,1042]],[[2051,910],[2119,974],[2199,962]],[[2627,1434],[2607,1466],[2643,1486],[2667,1446]],[[1659,949.999],[1747,933.999],[1751,961.999],[1691,977.999]],[[2883,2322],[2859,2294],[2859,2250],[2823,2238],[2759,2318],[2671,2318],[2643,2338],[2563,2354],[2543,2374],[2563,2374],[2567,2386],[2459,2414],[2459,2406],[2475,2382],[2491,2370],[2503,2318],[2539,2338],[2519,2290],[2427,2274],[2419,2266],[2411,2222],[2327,2186],[2283,2218],[2143,2186],[1415,2186],[1347,2146],[1395,2190],[1395,2206],[1335,2186],[1267,2126],[1283,2122],[1323,2134],[1207,2014],[1215,1986],[1227,1938],[1079,1798],[1031,1830],[935,1770],[935,1374],[1083,1410],[1239,1350],[1219,1374],[1291,1346],[1347,1386],[1367,1354],[1643,1430],[1611,1454],[1687,1462],[1751,1450],[1795,1474],[1807,1446],[1859,1426],[1787,1434],[1795,1422],[1859,1410],[1871,1430],[1955,1462],[2047,1454],[2047,1434],[2079,1426],[2031,1402],[2071,1358],[2127,1414],[2091,1426],[2119,1442],[2115,1478],[2135,1446],[2179,1422],[2155,1374],[2107,1342],[2139,1262],[2123,1178],[2175,1154],[2267,1166],[2223,1226],[2179,1222],[2167,1270],[2231,1346],[2207,1370],[2227,1386],[2271,1382],[2335,1438],[2347,1486],[2395,1418],[2395,1366],[2503,1394],[2479,1438],[2507,1474],[2435,1546],[2363,1518],[2355,1570],[2251,1646],[2159,1754],[2151,1826],[2191,1838],[2211,1898],[2259,1890],[2411,1970],[2479,1970],[2479,2054],[2535,2106],[2579,2070],[2543,1986],[2623,1938],[2611,1862],[2575,1834],[2615,1786],[2591,1686],[2735,1706],[2811,1754],[2827,1830],[2863,1854],[2911,1830],[2943,1762],[3059,1958],[3135,1990],[3179,2070],[3071,2142],[2891,2142],[2787,2242],[2899,2178],[2951,2186],[2939,2270],[3011,2294],[3047,2250],[3059,2290],[2919,2366],[2899,2338],[2947,2294]],[[2387,1558],[2355,1638],[2407,1658],[2455,1618],[2511,1642],[2523,1630]],[[1999,1174],[2083,1166],[2099,1270],[2043,1298],[1943,1222],[2011,1222]],[[2339,982],[2287,1026],[2563,1034],[2563,990],[2687,898],[2659,870],[2783,838],[3031,682],[2691,622],[2475,658],[2459,694],[2411,666],[2223,730],[2383,798],[2531,790],[2359,814],[2443,894],[2367,894],[2339,942],[2403,962],[2327,962]],[[2199,746],[2307,826],[2403,870],[2303,938],[2223,934],[2103,830]]],"cd":[[[5407,3678],[5399,3662],[5399,3650],[5419,3622],[5419,3586],[5463,3530],[5447,3522],[5451,3494],[5359,3454],[5243,3458],[5231,3478],[5175,3470],[5155,3450],[5127,3474],[5127,3494],[5127,3506],[5103,3606],[5063,3646],[5063,3678],[5055,3690],[5027,3714],[5019,3714],[5019,3702],[4999,3706],[4991,3714],[4983,3710],[4967,3722],[4967,3738],[4959,3738],[4967,3746],[4979,3742],[5075,3742],[5099,3798],[5147,3798],[5151,3770],[5179,3770],[5179,3778],[5211,3778],[5223,3886],[5271,3878],[5279,3878],[5283,3890],[5303,3886],[5307,3894],[5347,3906],[5351,3894],[5407,3942],[5423,3942],[5423,3910],[5403,3914],[5383,3890],[5391,3870],[5387,3834],[5399,3814],[5443,3802],[5411,3750],[5403,3718]]],"cf":[[[5127,3494],[5127,3474],[5155,3450],[5175,3470],[5231,3478],[5243,3458],[5359,3454],[5243,3298],[5127,3374],[5047,3386],[5019,3430],[5063,3530],[5075,3494]]],"cg":[[[5063,3530],[5075,3494],[5127,3494],[5127,3506],[5103,3606],[5063,3646],[5063,3678],[5055,3690],[5027,3714],[5019,3714],[5019,3702],[4999,3706],[4991,3714],[4983,3710],[4975,3702],[4955,3722],[4931,3690],[4943,3678],[4951,3686],[4943,3650],[4967,3646],[4967,3638],[4975,3634],[4983,3650],[5011,3650],[5019,3602],[5003,3594],[5019,3554],[4987,3554],[4987,3530],[5023,3530],[5063,3546]]],"ch":[[[4839,2238],[4823,2242],[4799,2282],[4819,2274],[4823,2290],[4847,2290],[4859,2274],[4875,2294],[4887,2274],[4907,2282],[4915,2262],[4891,2250],[4895,2238]]],"ci":[[[4567,3454],[4555,3414],[4575,3370],[4567,3342],[4547,3322],[4527,3334],[4495,3314],[4479,3322],[4475,3310],[4455,3322],[4431,3318],[4419,3390],[4415,3414],[4443,3438],[4439,3474],[4491,3454],[4543,3450]]],"cl":[[[2843,5106],[2803,5094],[2751,5094],[2739,5046],[2719,5050],[2711,5002],[2763,4866],[2747,4766],[2763,4666],[2779,4650],[2799,4494],[2787,4434],[2839,4306],[2839,4250],[2871,4230],[2875,4198],[2855,4198],[2815,4050],[2791,4074],[2799,4162],[2759,4406],[2763,4474],[2719,4606],[2703,4614],[2715,4678],[2699,4734],[2703,4750],[2695,4754],[2683,4802],[2703,4810],[2715,4750],[2731,4750],[2707,4854],[2699,4818],[2683,4834],[2679,4878],[2651,4914],[2687,4918],[2675,4950],[2655,4958],[2659,5050],[2671,5038],[2663,5078],[2691,5078],[2667,5086],[2715,5130],[2675,5118],[2739,5182],[2823,5210],[2927,5198],[2927,5190],[2899,5182],[2847,5142],[2831,5110],[2815,5110],[2791,5130],[2791,5142],[2819,5142],[2799,5154],[2803,5166],[2827,5182],[2795,5170],[2787,5150],[2775,5166],[2743,5150],[2771,5158],[2783,5122],[2819,5102]]],"cm":[[[4895,3530],[5023,3530],[5063,3546],[5063,3530],[5019,3430],[5047,3386],[5007,3330],[5015,3322],[5047,3322],[5035,3294],[5039,3270],[5019,3242],[5011,3242],[5011,3262],[5019,3262],[5023,3282],[5007,3290],[4939,3418],[4915,3402],[4863,3462],[4903,3502]]],"cn":[[[6947,2182],[6935,2186],[6891,2210],[6891,2250],[6819,2250],[6795,2314],[6747,2322],[6747,2410],[6691,2446],[6659,2450],[6643,2466],[6607,2466],[6583,2478],[6579,2494],[6575,2494],[6583,2522],[6603,2526],[6607,2566],[6635,2582],[6639,2606],[6683,2618],[6715,2650],[6707,2658],[6731,2698],[6715,2710],[6707,2702],[6699,2706],[6711,2738],[6767,2770],[6795,2770],[6895,2830],[6955,2838],[6971,2830],[6971,2862],[7003,2826],[7047,2842],[7075,2834],[7127,2798],[7147,2802],[7163,2794],[7195,2826],[7207,2818],[7231,2846],[7231,2894],[7203,2926],[7203,2946],[7235,2942],[7239,2970],[7255,2974],[7243,2998],[7267,3002],[7271,3018],[7295,3014],[7299,3022],[7311,3026],[7307,2994],[7311,2990],[7323,2990],[7331,2982],[7347,2990],[7407,2966],[7443,2982],[7443,3002],[7475,3018],[7491,3010],[7527,3018],[7519,3030],[7531,3050],[7543,3042],[7535,3030],[7623,2994],[7623,2978],[7631,2986],[7643,2986],[7707,2966],[7779,2894],[7847,2782],[7811,2770],[7843,2750],[7835,2718],[7819,2702],[7799,2650],[7771,2634],[7843,2570],[7855,2574],[7859,2558],[7847,2554],[7835,2558],[7815,2542],[7779,2566],[7763,2554],[7763,2534],[7735,2534],[7735,2506],[7763,2506],[7771,2498],[7823,2450],[7851,2462],[7831,2490],[7835,2506],[7823,2514],[7823,2518],[7887,2482],[7907,2482],[7975,2422],[8003,2438],[8007,2418],[8047,2402],[8055,2386],[8071,2406],[8071,2390],[8083,2386],[8083,2326],[8103,2310],[8135,2322],[8179,2214],[8079,2234],[8051,2178],[7991,2162],[7947,2058],[7879,2030],[7815,2038],[7791,2058],[7791,2066],[7811,2066],[7811,2086],[7735,2170],[7703,2158],[7675,2218],[7683,2234],[7755,2222],[7791,2258],[7783,2270],[7723,2270],[7627,2326],[7583,2318],[7567,2342],[7579,2362],[7515,2402],[7455,2406],[7399,2430],[7287,2394],[7175,2394],[7139,2342],[7087,2322],[7027,2314],[7019,2302],[7031,2286],[7007,2226],[6951,2202]],[[7545,3053.5],[7510,3059.5],[7493,3075.75],[7495,3096.5],[7518.25,3106],[7541.5,3090.25],[7555,3063.5]]],"co":[[[2595,3398],[2611,3378],[2607,3358],[2623,3366],[2671,3294],[2715,3290],[2759,3258],[2771,3270],[2751,3282],[2711,3346],[2759,3402],[2799,3402],[2815,3426],[2867,3422],[2855,3470],[2871,3498],[2859,3510],[2875,3526],[2883,3558],[2867,3534],[2855,3542],[2803,3542],[2803,3558],[2819,3558],[2819,3570],[2799,3570],[2799,3594],[2819,3622],[2803,3698],[2783,3686],[2799,3658],[2755,3646],[2719,3650],[2703,3618],[2663,3590],[2571,3550],[2611,3490],[2603,3478],[2607,3410]]],"cr":[[[2443,3302],[2435,3306],[2411,3294],[2391,3290],[2387,3294],[2383,3318],[2403,3334],[2415,3330],[2459,3370],[2467,3334]]],"cu":[[[2423,2990],[2479,2966],[2575,2986],[2691,3050],[2599,3058],[2611,3038],[2483,2982],[2419,3006]],[[2459,3002],[2455,3014],[2471,3014]]],"cy":[[[5547.67,2608.5],[5524.5,2618.5],[5504.17,2617.33],[5500.67,2625.5],[5488.33,2627.33],[5505.5,2642.5],[5532.33,2628.67],[5531.33,2620.33]]],"cz":[[[5135,2170],[5087,2198],[5035,2186],[5027,2202],[5003,2198],[4963,2146],[5031,2122],[5127,2158]]],"de":[[[4823,2038],[4827,2026],[4871,2018],[4867,1982],[4891,1982],[4907,1998],[4931,1998],[4927,2014],[4983,1998],[5011,2018],[5019,2046],[5011,2054],[5023,2066],[5031,2122],[4963,2146],[5003,2198],[4975,2218],[4979,2238],[4839,2238],[4855,2190],[4807,2174],[4811,2162],[4799,2150],[4807,2142],[4799,2126],[4787,2126],[4803,2102],[4795,2090],[4819,2086],[4827,2042]]],"dj":[[[5771,3254],[5751,3258],[5739,3278],[5739,3298],[5767,3298],[5775,3286],[5763,3282],[5779,3270]]],"dk":[[[4867,1982],[4891,1982],[4895,1958],[4915,1978],[4923,1962],[4907,1954],[4899,1958],[4899,1954],[4927,1922],[4911,1918],[4915,1874],[4887,1898],[4867,1898],[4851,1922],[4851,1958],[4867,1962]],[[4967,1938],[4951,1942],[4951,1950],[4943,1942],[4931,1954],[4935,1970],[4959,1978],[4959,1970],[4967,1966],[4959,1958],[4971,1954]]],"dk":[[[4867,1982],[4891,1982],[4895,1958],[4915,1978],[4923,1962],[4907,1954],[4899,1958],[4899,1954],[4927,1922],[4911,1918],[4915,1874],[4887,1898],[4867,1898],[4851,1922],[4851,1958],[4867,1962]],[[4967,1938],[4951,1942],[4951,1950],[4943,1942],[4931,1954],[4935,1970],[4959,1978],[4959,1970],[4967,1966],[4959,1958],[4971,1954]],[[4931,1978],[4927,1986],[4943,1994],[4955,1990],[4963,1978],[4947,1978],[4943,1982]]],"dm":[[[3024.5,3173.5],[3030.62,3177.12],[3030.38,3184.25],[3027.62,3185.25]]],"do":[[[2755,3062],[2755,3106],[2763,3118],[2787,3098],[2839,3102],[2847,3094],[2779,3058]]],"dz":[[[4863,2574],[4859,2626],[4835,2666],[4879,2718],[4887,2770],[4887,2886],[4955,2958],[4831,3034],[4795,3074],[4751,3082],[4723,3082],[4667,3026],[4506.85,2917.81],[4411,2854],[4411,2842],[4411,2814],[4539,2742],[4539,2726],[4607,2714],[4583,2630],[4719,2574]]],"ec":[[[2663,3590],[2571,3550],[2515,3614],[2515,3646],[2543,3666],[2531,3678],[2527,3706],[2551,3706],[2563,3722],[2591,3666],[2627,3654],[2663,3614]]],"ee":[[[5231,1826],[5219,1830],[5231,1838],[5215,1846],[5219,1870],[5251,1842],[5235,1838],[5243,1830]],[[5279,1870],[5303,1862],[5335,1882],[5347,1878],[5355,1882],[5367,1870],[5359,1834],[5375,1810],[5315,1802],[5255,1818],[5263,1854],[5283,1854]]],"eg":[[[5539,2738],[5559,2790],[5539,2838],[5491,2778],[5575,2970],[5531,3010],[5511,3002],[5299,3002],[5299,2730],[5318.47,2729.77],[5403,2754],[5455,2734],[5507,2746]]],"eh":[[[4411,2854],[4411,2842],[4295,2842],[4279,2874],[4259,2886],[4247,2926],[4195,3006],[4195,3022],[4295,3022],[4295,2974],[4323,2962],[4323,2890],[4411,2890]]],"er":[[[5651,3110],[5611,3138],[5599,3210],[5627,3214],[5635,3194],[5651,3206],[5695,3206],[5751,3258],[5771,3254],[5719,3198],[5695,3190],[5687,3178],[5683,3182],[5671,3162],[5659,3122]]],"es":[[[4719,2402],[4723,2422],[4655,2462],[4631,2498],[4580.09,2579.73],[4499,2594],[4495,2598],[4459,2566],[4443,2566],[4447,2494],[4475,2430],[4407,2414],[4395,2386],[4435,2358],[4595,2374],[4679,2398],[4679,2402],[4687,2398]],[[4721.33,2478.5],[4730.83,2486],[4720.67,2501.67],[4700.17,2490.17]],[[4739.67,2477.67],[4748.33,2474.5],[4752.83,2483.67]],[[4671.33,2510],[4679.67,2504],[4682.33,2507],[4675.33,2513.17]]],"et":[[[5583,3466],[5579,3446],[5507,3382],[5511,3366],[5531,3366],[5543,3302],[5555,3306],[5599,3210],[5627,3214],[5635,3194],[5651,3206],[5695,3206],[5751,3258],[5739,3278],[5739,3298],[5767,3298],[5759,3306],[5791,3346],[5871,3378],[5899,3378],[5823,3458],[5795,3458],[5739,3482],[5719,3482],[5711,3474],[5679,3498],[5639,3494],[5607,3470]]],"fi":[[[5271,1550],[5259,1454],[5183,1406],[5199,1390],[5239,1422],[5271,1414],[5295,1426],[5335,1362],[5371,1358],[5403,1374],[5411,1382],[5399,1402],[5383,1414],[5395,1414],[5387,1426],[5395,1446],[5427,1466],[5403,1498],[5431,1554],[5423,1558],[5419,1582],[5443,1622],[5427,1638],[5471,1670],[5459,1690],[5371,1766],[5247,1794],[5199,1766],[5207,1726],[5195,1678],[5287,1590],[5307,1586],[5303,1562]]],"fj":[[[9318.16,4045.3],[9292.48,4064.04],[9317.84,4074.22],[9334.15,4066.62]],[[9327.69,4032.22],[9334.48,4038.84],[9373.56,4033.68],[9362.26,4015.58]]],"fr":[[[4595,2374],[4679,2398],[4687,2398],[4719,2402],[4723,2378],[4751,2370],[4803,2382],[4824.67,2368.45],[4835,2362],[4839,2346],[4819,2342],[4823,2330],[4815,2318],[4827,2306],[4819,2274],[4799,2282],[4823,2242],[4839,2238],[4855,2190],[4807,2174],[4791,2170],[4783,2170],[4707,2118],[4679,2126],[4679,2150],[4635,2178],[4611,2174],[4607,2166],[4591,2166],[4599,2202],[4567,2202],[4555,2194],[4515,2202],[4527,2230],[4575,2238],[4611,2302],[4607,2342]],[[4887.88,2382.5],[4890.5,2411.62],[4881.12,2434.38],[4866,2418],[4866.62,2399.88],[4883.5,2392.12]]],"fk":[[[3119,5070],[3047,5070],[3051,5090],[3035,5090],[3047,5102],[3083,5074],[3067,5106],[3123,5082]]],"ga":[[[4931,3690],[4943,3678],[4951,3686],[4943,3650],[4967,3646],[4967,3638],[4975,3634],[4983,3650],[5011,3650],[5019,3602],[5003,3594],[5019,3554],[4987,3554],[4987,3530],[4939,3530],[4939,3562],[4891,3562],[4887,3586],[4871,3610],[4883,3638]]],"gb":[[[4647,2130],[4675,2114],[4675,2106],[4653,2106],[4683,2082],[4687,2062],[4671,2050],[4651,2050],[4647,2058],[4639,2054],[4647,2046],[4627,1998],[4607,1990],[4595,1954],[4559,1938],[4591,1878],[4535,1878],[4559,1842],[4507,1842],[4483,1886],[4475,1930],[4515,1958],[4507,1986],[4547,1982],[4563,2038],[4531,2038],[4523,2034],[4519,2038],[4523,2046],[4515,2058],[4531,2054],[4531,2078],[4503,2086],[4507,2098],[4555,2106],[4571.17,2096.17],[4559,2110],[4527,2114],[4495,2150],[4503,2154],[4515,2142],[4531,2142],[4543,2146],[4551,2134],[4567,2130],[4607,2134],[4619,2126]],[[4451,1974],[4427,1998],[4447,2010],[4455,1998],[4475,2014],[4499,1998],[4479,1966]]],"gd":[[[3018.62,3264.12],[3022.5,3267.5],[3016.38,3271.12]]],"ge":[[[5731,2430],[5759,2430],[5783,2442],[5823,2438],[5831,2434],[5863,2446],[5867,2438],[5855,2426],[5859,2418],[5839,2410],[5843,2402],[5695,2366],[5691,2374],[5727,2394],[5735,2426]]],"gf":[[[3223,3434],[3207,3458],[3223,3494],[3207,3526],[3247,3530],[3283,3478],[3255,3450]]],"gh":[[[4671,3426],[4655,3410],[4651,3318],[4635,3305.33],[4643,3294],[4635,3293.67],[4627,3298],[4567,3298],[4567,3342],[4575,3370],[4555,3414],[4567,3454],[4587,3462]]],"gi":[[[4499,2594],[4500.58,2596.42],[4498.31,2596.91]]],"gl":[[[2875,806],[2935,834],[2727,930],[2851,986],[2775,998],[2859,1062],[3007,1042],[3152.73,1129.71],[3194.33,1212],[3184.33,1289.33],[3242.67,1270.33],[3303,1330],[3207.19,1321.19],[3231,1390],[3307,1370],[3227,1470],[3351,1730],[3491,1786],[3591,1574],[3711,1538],[3791,1434],[4051,1358],[4047,1202],[4135,1138],[4115,878],[4323,730],[4195,706],[4047,746],[4063,674],[3767,590],[3351,646],[3387,694],[3275,662],[3287,698],[3179,682],[3011,722],[3015,762],[2975,758]]],"gm":[[[4199,3242],[4243,3230],[4267,3238],[4279,3234],[4247,3222],[4203,3230]]],"gn":[[[4291,3350],[4315,3326],[4347,3326],[4363,3358],[4359,3366],[4371,3362],[4387,3366],[4391,3390],[4403,3398],[4411,3386],[4419,3390],[4431,3318],[4395,3266],[4343,3274],[4339,3258],[4279,3254],[4279,3278],[4251,3286],[4247,3298]]],"gp":[[[3016.75,3154.25],[3022.88,3155.25],[3025.5,3149.5],[3032.62,3157.38],[3022.88,3159.5],[3018.5,3165.62]]],"gq":[[[4891,3562],[4939,3562],[4939,3530],[4895,3530],[4887,3558]],[[4875,3490],[4867,3502],[4859,3498],[4867,3490]]],"gr":[[[5171,2494],[5191,2462],[5191,2454],[5243,2438],[5283,2430],[5303,2442],[5327,2438],[5331,2426],[5327,2458],[5299,2450],[5263,2458],[5271,2470],[5251,2474],[5239,2466],[5231,2478],[5283,2538],[5271,2538],[5271,2554],[5259,2542],[5247,2546],[5259,2558],[5247,2562],[5235,2554],[5247,2590],[5235,2578],[5231,2590],[5219,2570],[5215,2578],[5207,2566],[5211,2562],[5195,2542],[5211,2530],[5243,2542],[5247,2538],[5215,2530],[5195,2530]],[[5259,2614],[5259,2622],[5295.2,2630.39],[5331,2630],[5331,2622]]],"gt":[[[2303,3166],[2299,3166],[2299,3114],[2251,3114],[2251,3130],[2239,3130],[2263,3162],[2231,3162],[2215,3202],[2243,3218],[2271,3222],[2291,3206],[2323,3170]]],"gw":[[[4247,3298],[4251,3286],[4279,3278],[4279,3254],[4239,3254],[4199,3262],[4223,3278],[4235,3278],[4231,3286]]],"gy":[[[3071,3366],[3043,3390],[3055,3402],[3027,3430],[3047,3450],[3063,3454],[3071,3538],[3095,3554],[3155,3538],[3115,3478],[3139,3434]]],"hk":[[[7631,2986],[7643,2986],[7639,2994]]],"hm":[[[6563,5126],[6579,5130],[6567,5134]]],"hn":[[[2291,3206],[2335,3222],[2331,3238],[2347,3246],[2411,3194],[2455,3190],[2423,3166],[2323,3170]]],"hr":[[[5135,2294],[5107,2294],[5071,2270],[5051,2286],[5043,2306],[4999,2306],[5003,2326],[5123,2402],[5051,2326],[5055,2314],[5143,2322]]],"ht":[[[2755,3062],[2755,3106],[2687,3102],[2687,3094],[2739,3094],[2731,3074],[2711,3066],[2727,3058]]],"hu":[[[5223,2210],[5239,2222],[5223,2230],[5195,2282],[5151,2282],[5135,2294],[5107,2294],[5071,2270],[5067,2258],[5087,2230],[5087,2222],[5103,2230],[5135,2226],[5135,2222],[5179,2206]]],"id":[[[7139,3442],[7199,3454],[7219,3482],[7363,3582],[7355,3606],[7383,3618],[7395,3650],[7415,3654],[7423,3670],[7415,3742],[7375,3734],[7323,3694],[7259,3590],[7243,3582],[7235,3546],[7183,3494],[7171,3490]],[[7399,3642],[7419,3630],[7431,3654],[7443,3658],[7439,3674],[7423,3662],[7419,3646]],[[7467,3658],[7463,3674],[7479,3674],[7483,3662]],[[7423,3746],[7407,3770],[7435,3774],[7431,3782],[7647,3822],[7643,3798],[7607,3790],[7599,3778],[7635,3774],[7551,3762],[7539,3770],[7491,3766],[7483,3750]],[[7735,3482],[7679,3478],[7647,3550],[7623,3558],[7591,3550],[7583,3562],[7543,3566],[7519,3538],[7499,3562],[7515,3622],[7523,3618],[7535,3666],[7575,3666],[7575,3682],[7647,3682],[7647,3698],[7687,3686],[7735,3562],[7763,3562],[7723,3506]],[[7919,3542],[7931,3550],[7907,3578],[7803,3578],[7791,3606],[7819,3626],[7831,3610],[7879,3606],[7831,3646],[7875,3710],[7867,3738],[7855,3726],[7863,3706],[7839,3718],[7831,3714],[7819,3662],[7795,3670],[7807,3738],[7775,3734],[7779,3682],[7759,3678],[7795,3570],[7819,3558],[7899,3570]],[[7647,3806],[7667,3802],[7679,3810],[7667,3822]],[[7691,3806],[7703,3810],[7695,3822],[7683,3818]],[[7711,3810],[7707,3830],[7747,3818]],[[7731,3802],[7763,3806],[7767,3818],[7751,3822]],[[7783,3818],[7803,3806],[7851,3818],[7923,3802],[7927,3810],[7835,3822]],[[7919,3826],[7923,3838],[7887,3866],[7879,3858],[7891,3838]],[[7803,3862],[7811,3854],[7787,3834],[7763,3838]],[[7951,3674],[7975,3670],[7983,3686],[7971,3690]],[[8003,3666],[8059,3666],[8079,3690],[8023,3674],[8011,3682]],[[8015,3522],[8007,3528.86],[7987,3546],[7995,3598],[8011,3614],[7999,3586],[7999,3574],[8019,3582],[8019,3578],[8003,3570],[8019,3558],[8019,3546],[7995,3570],[7995,3562],[8003,3546],[7999,3542]],[[7991,3626],[7987,3634],[8003,3634]],[[7907,3634],[7959,3638],[7907,3642]],[[8079,3622],[8119,3598],[8159,3606],[8171,3658],[8183,3674],[8199,3678],[8259,3630],[8343,3658],[8343,3826],[8311,3802],[8255,3810],[8267,3786],[8287,3782],[8263,3730],[8147,3682],[8139,3698],[8111,3662],[8155,3650],[8111,3646],[8103,3630]],[[8171,3734],[8163,3754],[8163,3766],[8179,3754]],[[8095,3778],[8083,3794],[8083,3802],[8095,3786]]],"ie":[[[4475,2014],[4455,1998],[4447,2010],[4427,1998],[4451,1974],[4455,1966],[4423,1970],[4407,1990],[4419,1994],[4411,2002],[4375,2002],[4375,2034],[4395,2046],[4367,2078],[4383,2102],[4471,2078],[4483,2050]]],"il":[[[5563,2686],[5575,2682],[5575,2698],[5571,2746],[5559,2790],[5539,2738],[5551,2722]]],"in":[[[6431,2950],[6507,2934],[6495,2898],[6487,2898],[6483,2874],[6471,2874],[6467,2862],[6491,2834],[6499,2842],[6527,2834],[6599,2746],[6599,2726],[6619,2710],[6603,2706],[6579,2650],[6591,2638],[6631,2646],[6683,2618],[6715,2650],[6707,2658],[6731,2698],[6715,2710],[6707,2702],[6699,2706],[6711,2738],[6767,2770],[6739,2806],[6827,2854],[6847,2850],[6863,2854],[6863,2862],[6883,2870],[6951,2878],[6955,2838],[6971,2830],[6971,2862],[6999,2870],[7059,2866],[7055,2850],[7047,2850],[7047,2842],[7075,2834],[7127,2798],[7147,2802],[7163,2794],[7195,2826],[7195,2838],[7183,2850],[7191,2862],[7175,2854],[7139,2874],[7111,2950],[7091,2946],[7087,2994],[7071,3002],[7063,2954],[7043,2978],[7035,2954],[7067,2918],[6999,2910],[6995,2886],[6963,2878],[6955,2894],[6975,2910],[6951,2930],[6971,2938],[6979,3006],[6927,3014],[6923,3038],[6743,3182],[6751,3238],[6735,3282],[6735,3318],[6723,3318],[6715,3342],[6695,3350],[6691,3370],[6675,3378],[6651,3354],[6643,3326],[6571,3166],[6547,3062],[6555,3042],[6543,2998],[6531,3026],[6499,3038],[6451,2994],[6483,2986],[6483,2978],[6467,2982]]],"iq":[[[5815,2566],[5755,2566],[5735,2586],[5727,2586],[5715,2650],[5659,2678],[5671,2714],[5743,2742],[5815,2802],[5863,2802],[5879,2778],[5911,2778],[5895,2726],[5835,2658],[5851,2606],[5831,2602]]],"ir":[[[5911,2778],[5895,2726],[5835,2658],[5851,2606],[5831,2602],[5815,2566],[5799,2498],[5811,2486],[5835,2510],[5851,2514],[5859,2514],[5899,2490],[5923,2530],[5931,2554],[6003,2586],[6059,2574],[6055,2562],[6123,2530],[6247,2582],[6251,2614],[6231,2686],[6239,2734],[6263,2738],[6263,2754],[6239,2782],[6299,2854],[6299,2874],[6263,2882],[6255,2914],[6143,2898],[6131,2858],[6075,2874],[5995,2838],[5951,2770]]],"is":[[[4019,1586],[4067,1614],[4047,1634],[4147,1654],[4283,1582],[4215,1518],[4107,1534],[4083,1566],[4043,1518],[3999,1562],[4059,1566]]],"it":[[[4835,2362],[4871,2338],[4983,2442],[5051,2478],[5067,2514],[5051,2534],[5059,2546],[5091,2502],[5075,2486],[5091,2466],[5119,2486],[5127,2474],[5075,2442],[5059,2430],[5063,2418],[5035,2418],[4995,2366],[4963,2346],[4963,2306],[4995,2294],[4999,2270],[4959,2254],[4915,2262],[4907,2282],[4887,2274],[4875,2294],[4859,2274],[4847,2290],[4823,2290],[4827,2306],[4815,2318],[4823,2330],[4819,2342],[4839,2346]],[[5047,2534],[5035,2578],[4963,2546]],[[4887,2442],[4899,2466],[4891,2506],[4859,2506],[4855,2454]]],"jm":[[[2579,3102],[2607,3094],[2639,3110],[2611,3118]]],"jo":[[[5659,2678],[5607,2710],[5575,2698],[5571,2746],[5559,2790],[5559,2798],[5587,2802],[5639,2762],[5611,2734],[5671,2714]]],"jp":[[[8359,2310],[8351,2378],[8311,2394],[8319,2430],[8339,2422],[8327,2402],[8367,2394],[8399,2414],[8463,2374],[8455,2342],[8427,2354],[8367,2302]],[[8159,2650],[8179,2662],[8163,2682],[8147,2674],[8131,2698],[8115,2678],[8131,2658],[8139,2666],[8147,2662],[8151,2654]],[[8079,2662],[8111,2690],[8087,2738],[8075,2750],[8059,2738],[8067,2698],[8039,2682]],[[8327,2438],[8339,2434],[8375,2498],[8343,2542],[8335,2606],[8223,2650],[8207,2678],[8183,2654],[8195,2638],[8119,2654],[8111,2662],[8079,2658],[8083,2646],[8127,2614],[8191,2610],[8207,2618],[8215,2610],[8211,2598],[8239,2566],[8247,2578],[8299,2534],[8319,2482],[8315,2458]]],"ke":[[[5531,3478],[5543,3466],[5583,3466],[5607,3470],[5639,3494],[5679,3498],[5711,3474],[5719,3482],[5739,3482],[5715,3514],[5715,3610],[5731,3630],[5707,3654],[5695,3658],[5695,3674],[5671,3710],[5627,3678],[5631,3670],[5535,3614],[5531,3582],[5563,3534]]],"kg":[[[6575,2494],[6499,2498],[6463,2494],[6459,2486],[6479,2474],[6495,2482],[6503,2474],[6523,2478],[6559,2454],[6523,2434],[6515,2442],[6487,2430],[6511,2414],[6503,2410],[6519,2390],[6567,2402],[6587,2378],[6627,2390],[6719,2390],[6747,2410],[6691,2446],[6659,2450],[6643,2466],[6607,2466],[6583,2478],[6579,2494]]],"kh":[[[7383,3314],[7415,3298],[7427,3306],[7419,3282],[7463,3262],[7463,3202],[7447,3210],[7439,3206],[7427,3210],[7427,3218],[7423,3222],[7403,3210],[7351,3210],[7327,3234],[7339,3274],[7347,3290],[7347,3302],[7355,3302],[7359,3298],[7363,3306],[7359,3310],[7363,3314],[7367,3310]]],"kp":[[[7907,2482],[7975,2422],[8003,2438],[8007,2418],[8047,2402],[8055,2386],[8071,2406],[8043,2430],[8047,2454],[7987,2486],[7987,2506],[8011,2522],[7967,2546],[7919,2538],[7931,2494]]],"kr":[[[8011,2522],[8043,2574],[8031,2630],[7963,2650],[7955,2630],[7967,2598],[7955,2578],[7971,2570],[7967,2546]]],"kw":[[[5911,2778],[5895,2794],[5911,2818],[5891,2818],[5887,2806],[5863,2802],[5879,2778]]],"ky":[[[2482.74,3012.25],[2490.15,3008.87],[2486.86,3012.25]],[[2496.75,3010.45],[2498.82,3012.74],[2502.79,3008.19]]],"kz":[[[6111,2438],[6099,2442],[6067,2406],[6035,2410],[6019,2426],[6015,2410],[6027,2398],[6003,2390],[5975,2346],[5959,2342],[5959,2330],[5987,2334],[5979,2322],[5991,2310],[6027,2306],[6035,2266],[5983,2250],[5931,2274],[5863,2206],[5891,2138],[5911,2162],[5923,2154],[5919,2134],[5975,2094],[6015,2094],[6103,2134],[6175,2114],[6207,2138],[6251,2126],[6255,2106],[6215,2086],[6243,2074],[6235,2058],[6247,2050],[6267,2050],[6271,2046],[6247,2038],[6247,2018],[6463,1966],[6499,1966],[6511,2010],[6571,2018],[6571,2030],[6655,2002],[6743,2126],[6767,2110],[6799,2130],[6831,2118],[6903,2170],[6919,2166],[6935,2186],[6891,2210],[6891,2250],[6819,2250],[6795,2314],[6747,2322],[6747,2410],[6719,2390],[6627,2390],[6587,2378],[6567,2402],[6519,2390],[6503,2410],[6451,2438],[6439,2458],[6419,2442],[6395,2442],[6387,2418],[6375,2418],[6375,2386],[6367,2390],[6343,2362],[6267,2370],[6243,2342],[6179,2302],[6111,2322]]],"la":[[[7295,3014],[7267,3046],[7279,3054],[7279,3070],[7299,3070],[7291,3122],[7295,3126],[7323,3106],[7335,3118],[7355,3098],[7391,3130],[7391,3150],[7415,3174],[7411,3190],[7411,3206],[7403,3210],[7423,3222],[7427,3218],[7427,3210],[7439,3206],[7447,3210],[7463,3202],[7467,3186],[7455,3170],[7463,3166],[7399,3098],[7403,3094],[7371,3074],[7399,3054],[7375,3030],[7363,3038],[7343,3022],[7343,3010],[7323,2990],[7311,2990],[7307,2994],[7311,3026],[7299,3022]]],"lb":[[[5583,2642],[5591,2642],[5599,2654],[5575,2682],[5563,2686]]],"lc":[[[3038.88,3214.75],[3035.38,3222.88],[3039.88,3224.5]]],"lk":[[[6739,3330],[6763,3346],[6787,3390],[6791,3410],[6759,3434],[6743,3426],[6731,3378],[6743,3342]]],"lr":[[[4439,3474],[4443,3438],[4415,3414],[4419,3390],[4411,3386],[4403,3398],[4391,3390],[4387,3366],[4371,3362],[4339,3406],[4399,3454]]],"ls":[[[5375,4418],[5415,4382],[5391,4358],[5351,4390]]],"lt":[[[5199,1966],[5191,1938],[5223,1926],[5295,1926],[5339,1954],[5311,2006],[5259,2018],[5235,1998],[5235,1974]]],"lu":[[[4791,2158],[4791,2170],[4807,2174],[4811,2162],[4799,2150]]],"lv":[[[5191,1938],[5223,1926],[5295,1926],[5339,1954],[5379,1934],[5371,1890],[5347,1878],[5335,1882],[5303,1862],[5279,1870],[5279,1894],[5259,1906],[5231,1874],[5199,1894]]],"ly":[[[5299,2730],[5299,3002],[5299,3058],[5271,3058],[5271,3070],[5059,2962],[5035.03,2973.9],[5015,2986],[4995,2970],[4955,2958],[4887,2886],[4887,2770],[4911,2754],[4903,2734],[4943,2706],[4943,2686],[5039,2710],[5051,2734],[5143,2770],[5171,2742],[5163,2726],[5211,2690],[5247,2702],[5251,2714],[5295,2722]]],"ma":[[[4583,2630],[4607,2714],[4539,2726],[4539,2742],[4411,2814],[4411,2842],[4295,2842],[4375,2794],[4383,2734],[4415,2686],[4459,2662],[4483,2610],[4499,2606],[4507,2622]]],"mc":[[[4823,2370],[4824.88,2368.75],[4824.75,2370.25]]],"md":[[[5343,2210],[5383,2266],[5375,2282],[5387,2314],[5419,2310],[5439,2282],[5427,2270],[5403,2222],[5363,2206]]],"me":[[[5175,2386],[5167,2398],[5147,2422],[5123,2402],[5143,2370]]],"mg":[[[5827,4274],[5875,4258],[5967,3994],[5935,3902],[5875,3990],[5807,4018],[5791,4050],[5807,4118],[5779,4166],[5787,4218],[5803,4262]]],"mk":[[[5227,2406],[5239,2418],[5243,2438],[5191,2454],[5179,2442],[5183,2418]]],"ml":[[[4507,2918],[4467,2918],[4491,3150],[4499,3158],[4495,3178],[4359,3178],[4351,3190],[4335,3174],[4319,3202],[4339.03,3257.91],[4343,3274],[4395,3266],[4431,3318],[4455,3322],[4475,3310],[4479,3322],[4495,3314],[4499,3278],[4563,3218],[4583,3214],[4587,3206],[4615,3194],[4645.25,3194],[4665,3194],[4675,3182],[4731,3182],[4751,3154],[4751,3082],[4723,3082],[4667,3026]]],"mm":[[[7059,3030],[7071,3022],[7071,3002],[7087,2994],[7091,2946],[7111,2950],[7139,2874],[7175,2854],[7191,2862],[7183,2850],[7195,2838],[7195,2826],[7207,2818],[7231,2846],[7231,2894],[7203,2926],[7203,2946],[7235,2942],[7239,2970],[7255,2974],[7243,2998],[7267,3002],[7271,3018],[7295,3014],[7267,3046],[7215,3066],[7199,3098],[7207,3118],[7235,3154],[7227,3166],[7227,3182],[7219,3186],[7227,3210],[7247,3226],[7243,3242],[7255,3278],[7227,3326],[7227,3302],[7231,3282],[7231,3258],[7187,3130],[7179,3150],[7147,3174],[7115,3162],[7123,3122],[7107,3086],[7095,3074],[7099,3066],[7079,3054]]],"mn":[[[6947,2182],[6951,2202],[7007,2226],[7031,2286],[7019,2302],[7027,2314],[7087,2322],[7139,2342],[7175,2394],[7287,2394],[7399,2430],[7455,2406],[7515,2402],[7579,2362],[7567,2342],[7583,2318],[7627,2326],[7723,2270],[7783,2270],[7791,2258],[7755,2222],[7683,2234],[7675,2218],[7703,2158],[7639,2146],[7547,2182],[7403,2138],[7363,2150],[7327,2134],[7323,2106],[7239,2082],[7207,2122],[7219,2138],[7211,2154],[7195,2162],[7127,2154],[7115,2134],[7063,2126]]],"mq":[[[3031.62,3194.75],[3040.12,3197.5],[3041.25,3206.62],[3035.12,3204.88]]],"mr":[[[4195,3022],[4295,3022],[4295,2974],[4323,2962],[4323,2890],[4411,2890],[4411,2854],[4507,2918],[4467,2918],[4491,3150],[4499,3158],[4495,3178],[4359,3178],[4351,3190],[4335,3174],[4319,3202],[4263,3146],[4207,3158],[4219,3118],[4207,3074],[4215,3050]]],"ms":[[[3004,3142.62],[3007.12,3142],[3006.25,3145.5]]],"mt":[[[5013.88,2597.25],[5023.25,2603.5],[5018.25,2606]]],"mw":[[[5503,3830],[5535,3842],[5559,3894],[5555,3950],[5583,3974],[5579,4014],[5563,4026],[5567,4042],[5539,4006],[5547,3990],[5543,3970],[5523,3974],[5511,3958],[5523,3866]]],"mx":[[[2087,2890],[2039,2878],[1975,2782],[1951,2778],[1931,2806],[1843,2726],[1799,2726],[1799,2738],[1723,2738],[1627,2702],[1563,2702],[1599,2782],[1647,2822],[1639,2842],[1615,2838],[1691,2890],[1691,2922],[1755,2974],[1767,2962],[1627,2766],[1627,2722],[1663,2738],[1691,2806],[1875,3014],[1867,3046],[1919,3102],[2103,3174],[2139,3158],[2163,3162],[2215,3202],[2231,3162],[2263,3162],[2239,3130],[2251,3130],[2251,3114],[2299,3114],[2319,3094],[2335,3098],[2355,3010],[2267,3026],[2255,3074],[2227,3094],[2155,3106],[2143,3090],[2123,3090],[2067,2994]]],"my":[[[7271,3422],[7275,3418],[7295,3426],[7295,3442],[7315,3438],[7323,3426],[7355,3458],[7355,3514],[7363,3522],[7379,3550],[7363,3550],[7359,3554],[7299,3514],[7299,3502],[7283,3486]],[[7519,3538],[7543,3566],[7583,3562],[7591,3550],[7623,3558],[7647,3550],[7679,3478],[7735,3482],[7755,3474],[7743,3458],[7771,3450],[7711,3402],[7663,3458],[7671,3474],[7655,3462],[7655,3478],[7651,3482],[7639,3470],[7607,3506],[7567,3518],[7555,3550]]],"mz":[[[5703,3866],[5623,3894],[5559,3894],[5555,3950],[5583,3974],[5579,4014],[5563,4026],[5567,4042],[5539,4006],[5547,3990],[5543,3970],[5523,3974],[5511,3958],[5435,3986],[5439,4002],[5439,4010],[5503,4030],[5503,4126],[5463,4186],[5479,4286],[5483,4310],[5503,4310],[5503,4290],[5495,4286],[5575,4226],[5571,4178],[5551,4130],[5615,4058],[5687,4022],[5711,3978]]],"na":[[[5163,4254],[5163,4174],[5191,4174],[5191,4074],[5251,4066],[5259,4078],[5299,4058],[5279,4050],[5255,4058],[5187,4066],[5139,4062],[5123,4050],[5007,4050],[4983,4038],[4947,4046],[5019,4202],[5031,4298],[5071,4362],[5087,4342],[5099,4362],[5147,4370],[5163,4354]]],"nc":[[[8945,4124],[8983,4144],[9025,4185],[8974,4158]]],"ne":[[[4735,3278],[4711,3258],[4703,3274],[4644.5,3194],[4665.33,3194],[4675,3182],[4731,3182],[4751,3154],[4751,3082],[4795,3074],[4831,3034],[4955,2958],[4995,2970],[5015,2986],[5035,2974],[5059,3046],[5047,3138],[4995,3206],[4995,3226],[4967,3242],[4923,3234],[4891,3250],[4847,3234],[4823,3246],[4759,3222]]],"ng":[[[4863,3462],[4915,3402],[4939,3418],[5007,3290],[5023,3282],[5019,3262],[5011,3262],[5011,3242],[4995,3226],[4967,3242],[4923,3234],[4891,3250],[4847,3234],[4823,3246],[4759,3222],[4735,3278],[4731,3286],[4739,3314],[4719,3350],[4711,3350],[4711,3418],[4755,3418],[4799,3474]]],"ni":[[[2347,3246],[2411,3194],[2455,3190],[2435,3286],[2443,3302],[2435,3306],[2411,3294],[2411,3290],[2399,3274],[2383,3270],[2391,3290],[2387,3294],[2339,3250]]],"nl":[[[4795,2114],[4803,2102],[4795,2090],[4819,2086],[4827,2042],[4819,2034],[4787,2038],[4767,2050],[4759,2074],[4727,2106],[4771,2102]]],"no":[[[4931,1826],[4947,1826],[4951,1794],[4967,1786],[4963,1750],[4979,1734],[4959,1718],[4955,1658],[4979,1622],[5007,1626],[5011,1606],[4999,1602],[5019,1534],[5035,1534],[5067,1494],[5063,1478],[5095,1450],[5107,1454],[5115,1430],[5163,1438],[5167,1406],[5183,1406],[5199,1390],[5239,1422],[5271,1414],[5295,1426],[5335,1362],[5371,1358],[5403,1374],[5411,1382],[5399,1402],[5451,1370],[5395,1354],[5431,1358],[5455,1342],[5395,1318],[5375,1334],[5387,1314],[5355,1314],[5335,1342],[5339,1314],[5299,1350],[5319,1310],[5287,1314],[5247,1358],[5243,1346],[5135,1362],[4883,1646],[4779,1702],[4767,1746],[4787,1846],[4827,1866],[4855,1862],[4919,1810]]],"np":[[[6955,2838],[6951,2878],[6883,2870],[6863,2862],[6863,2854],[6847,2850],[6827,2854],[6739,2806],[6767,2770],[6795,2770],[6895,2830]]],"nz":[[[9179,4530],[9231,4630],[9223,4666],[9203,4678],[9243,4706],[9231,4738],[9247,4750],[9331,4630],[9299,4634],[9259,4622]],[[9175,4714],[9187,4738],[9219,4734],[9215,4758],[9175,4810],[9183,4822],[9139,4834],[9095,4910],[9011,4894],[9023,4866],[9127,4790],[9143,4754],[9159,4742],[9159,4726]]],"om":[[[6115,2890],[6119,2882],[6123,2882],[6119,2902]],[[6119,2922],[6107,2922],[6107,2946],[6099,2946],[6087,2986],[6103,3002],[6083,3058],[6003,3086],[6035,3150],[6159,3086],[6211,2986]]],"pa":[[[2467,3334],[2459,3370],[2491,3374],[2515,3398],[2539,3390],[2527,3370],[2555,3350],[2583,3366],[2579,3374],[2595,3398],[2611,3378],[2607,3358],[2575,3338],[2547,3334],[2507,3358]]],"pe":[[[2531,3678],[2527,3706],[2551,3706],[2563,3722],[2591,3666],[2627,3654],[2663,3614],[2663,3590],[2703,3618],[2719,3650],[2755,3646],[2799,3658],[2783,3686],[2803,3698],[2779,3698],[2727,3722],[2719,3758],[2695,3786],[2747,3854],[2787,3838],[2787,3878],[2815,3878],[2835,3918],[2819,3998],[2803,3994],[2803,4010],[2823,4018],[2827,4030],[2815,4050],[2791,4074],[2635,3958],[2639,3946],[2539,3766],[2507,3738],[2507,3698]]],"pg":[[[8343,3826],[8343,3658],[8459,3706],[8467,3730],[8511,3746],[8523,3766],[8499,3766],[8543,3830],[8559,3826],[8599,3866],[8519,3858],[8467,3798],[8435,3786],[8387,3838]],[[8527,3734],[8575,3758],[8635,3730],[8639,3702],[8619,3702],[8623,3718],[8599,3734]],[[8655,3718],[8659,3694],[8603,3658],[8647,3694]],[[8703,3730],[8739,3762],[8727,3770],[8707,3746]]],"ph":[[[7814,3092.33],[7848,3103],[7851.33,3151.67],[7826,3184.33],[7846,3217],[7900,3226.33],[7897.33,3251.67],[7841.33,3221.67],[7814,3227],[7786.67,3156.33],[7804.67,3160.33]],[[7801.33,3231],[7827.33,3232.33],[7826.67,3266.33]],[[7873.33,3273],[7878,3256.33],[7896.67,3271],[7881.33,3265.67]],[[7841.33,3274.33],[7878.67,3284.33],[7845.33,3309.67]],[[7854,3329],[7880.67,3298.33],[7877.33,3334.33],[7897.33,3293.67],[7896,3314.33],[7870.67,3347.67]],[[7902.67,3257.67],[7928,3253.67],[7942,3299]],[[7920,3285],[7906,3295],[7927.33,3323.67]],[[7902.67,3318.33],[7888.67,3339],[7908.67,3329.67]],[[7777.33,3285],[7784.67,3311.67],[7716.67,3369.67],[7772,3306.33]],[[7937,3318],[7933.5,3349.5],[7896,3371.5],[7885.5,3358],[7848.5,3377.5],[7843,3406.5],[7861,3385],[7867,3395],[7889,3379],[7903.5,3392.5],[7896.5,3421.5],[7934,3439.5],[7939,3417.5],[7931,3408.5],[7945,3391],[7952.5,3422],[7965.5,3383.5]],[[7837.5,3414],[7853,3412.5],[7845.5,3419]]],"pk":[[[6255,2914],[6263,2882],[6299,2874],[6299,2854],[6239,2782],[6279,2794],[6323,2794],[6383,2778],[6383,2750],[6431,2726],[6447,2730],[6459,2722],[6467,2690],[6487,2682],[6475,2662],[6499,2662],[6507,2654],[6503,2646],[6523,2626],[6511,2598],[6523,2586],[6555,2574],[6583,2578],[6607,2566],[6635,2582],[6639,2606],[6683,2618],[6631,2646],[6591,2638],[6579,2650],[6603,2706],[6619,2710],[6599,2726],[6599,2746],[6527,2834],[6499,2842],[6491,2834],[6467,2862],[6471,2874],[6483,2874],[6487,2898],[6495,2898],[6507,2934],[6431,2950],[6415,2950],[6387,2906]]],"pl":[[[5011,2018],[5123,1986],[5135,2002],[5235,1998],[5259,2018],[5267,2062],[5251,2078],[5259,2082],[5259,2098],[5271,2138],[5235,2170],[5239,2186],[5231,2186],[5135,2170],[5127,2158],[5031,2122],[5023,2066],[5011,2054],[5019,2046]]],"pr":[[[2875,3098],[2875,3110],[2911,3110],[2915,3098]]],"pt":[[[4407,2414],[4475,2430],[4447,2494],[4443,2566],[4403,2570],[4407,2526],[4391,2522],[4411,2442]]],"py":[[[2995,4182],[3019,4110],[3087,4102],[3115,4122],[3115,4178],[3175,4182],[3187,4230],[3215,4230],[3207,4274],[3199,4306],[3175,4326],[3099,4322],[3127,4270],[3047,4226]]],"qa":[[[5987,2934],[5975,2926],[5975,2898],[5987,2886],[5995,2894],[5995,2922]]],"ro":[[[5239,2222],[5223,2230],[5195,2282],[5171,2282],[5235,2346],[5243,2358],[5315,2362],[5355,2350],[5391,2362],[5403,2326],[5415,2326],[5419,2310],[5387,2314],[5375,2282],[5383,2266],[5343,2210],[5295,2234]]],"rs":[[[5171,2282],[5235,2346],[5227,2358],[5243,2378],[5227,2406],[5183,2418],[5167,2398],[5175,2386],[5143,2370],[5143,2322],[5135,2294],[5151,2282]]],"ru":[[[5135,2002],[5199,1966],[5235,1974],[5235,1998]],[[5375,1810],[5359,1834],[5367,1870],[5355,1882],[5371,1890],[5379,1934],[5451,1954],[5451,1986],[5499,2034],[5467,2042],[5475,2082],[5527,2070],[5539,2110],[5559,2110],[5575,2142],[5603,2146],[5623,2138],[5691,2170],[5683,2226],[5651,2230],[5643,2250],[5675,2254],[5631,2270],[5647,2282],[5619,2310],[5603,2306],[5599,2314],[5691,2374],[5695,2366],[5843,2402],[5839,2410],[5859,2418],[5899,2442],[5915,2422],[5867,2338],[5891,2298],[5919,2290],[5931,2274],[5863,2206],[5891,2138],[5911,2162],[5923,2154],[5919,2134],[5975,2094],[6015,2094],[6103,2134],[6175,2114],[6207,2138],[6251,2126],[6255,2106],[6215,2086],[6243,2074],[6235,2058],[6247,2050],[6267,2050],[6271,2046],[6247,2038],[6247,2018],[6463,1966],[6499,1966],[6511,2010],[6571,2018],[6571,2030],[6655,2002],[6743,2126],[6767,2110],[6799,2130],[6831,2118],[6903,2170],[6919,2166],[6935,2186],[6947,2182],[7063,2126],[7115,2134],[7127,2154],[7195,2162],[7211,2154],[7219,2138],[7207,2122],[7239,2082],[7323,2106],[7327,2134],[7363,2150],[7403,2138],[7547,2182],[7639,2146],[7703,2158],[7735,2170],[7811,2086],[7811,2066],[7791,2066],[7791,2058],[7815,2038],[7879,2030],[7947,2058],[7991,2162],[8051,2178],[8079,2234],[8179,2214],[8135,2322],[8103,2310],[8083,2326],[8083,2386],[8071,2390],[8071,2406],[8115,2382],[8135,2394],[8199,2358],[8319,2206],[8355,2038],[8287,2002],[8263,2030],[8195,1978],[8399,1814],[8715,1818],[8691,1798],[8771,1718],[8851,1714],[8847,1762],[8959,1682],[8963,1690],[8935,1750],[8763,1882],[8727,1962],[8755,2122],[8903,1986],[8895,1938],[8927,1934],[8919,1870],[8895,1866],[8967,1790],[9127,1770],[9299,1682],[9343,1698],[9359,1678],[9287,1594],[9363,1586],[9395,1542],[9547,1614],[9635,1534],[9519,1490],[9511,1506],[9503,1478],[9263,1366],[9111,1354],[9131,1406],[9091,1414],[9067,1362],[8871,1390],[8807,1314],[8643,1326],[8535,1250],[8339,1222],[8311,1290],[8163,1302],[8127,1270],[8075,1326],[8015,1206],[7883,1178],[7863,1218],[7611,1166],[7435,1206],[7627,1090],[7619,1046],[7551,1018],[7447,1030],[7455,1006],[7379,962],[7159,1066],[6991,1078],[6895,1138],[6907,1166],[6751,1190],[6771,1246],[6827,1282],[6763,1262],[6671,1238],[6639,1282],[6699,1314],[6615,1294],[6611,1230],[6559,1294],[6591,1338],[6583,1402],[6647,1410],[6595,1426],[6595,1470],[6539,1530],[6483,1522],[6543,1486],[6571,1430],[6547,1314],[6559,1222],[6487,1202],[6435,1278],[6391,1370],[6451,1414],[6435,1434],[6291,1374],[6215,1362],[6243,1410],[6203,1422],[6191,1410],[6035,1418],[5899,1470],[5891,1498],[5827,1486],[5863,1454],[5803,1426],[5787,1486],[5795,1534],[5743,1522],[5679,1562],[5695,1598],[5611,1574],[5603,1594],[5635,1614],[5627,1634],[5555,1606],[5551,1542],[5491,1502],[5495,1490],[5575,1526],[5647,1538],[5695,1530],[5727,1494],[5719,1466],[5587,1402],[5451,1370],[5399,1402],[5383,1414],[5395,1414],[5387,1426],[5395,1446],[5427,1466],[5403,1498],[5431,1554],[5423,1558],[5419,1582],[5443,1622],[5427,1638],[5471,1670],[5459,1690],[5371,1766],[5427,1786],[5379,1798]],[[5923,1382],[5903,1414],[5931,1418],[5959,1394]],[[6135,1338],[5995,1290],[6119,1110],[6247,1046],[6403,1010],[6451,1018],[6439,1050],[6243,1102],[6147,1182],[6095,1250],[6107,1298],[6155,1322]],[[7319,870],[7251,950],[7395,926]],[[7263,842],[7259,898],[7103,854]],[[7091,842],[7191,814],[7211,786],[7147,762],[7039,834]],[[8367,2034],[8367,2282],[8403,2266],[8387,2222],[8411,2174],[8431,2182],[8395,2018]]],"rw":[[[5439,3614],[5427,3626],[5419,3622],[5399,3650],[5399,3662],[5447,3650]]],"sa":[[[5911,2818],[5891,2818],[5887,2806],[5863,2802],[5815,2802],[5743,2742],[5671,2714],[5611,2734],[5639,2762],[5587,2802],[5559,2798],[5547,2830],[5559,2830],[5627,2938],[5651,2954],[5667,2994],[5663,3002],[5687,3050],[5707,3062],[5763,3154],[5775,3146],[5775,3130],[5783,3126],[5867,3134],[5879,3142],[5903,3110],[6003,3086],[6083,3058],[6103,3002],[6087,2986],[6023,2978],[5995,2942],[5987,2934],[5975,2926],[5955,2894],[5951,2866],[5923,2846]]],"sb":[[[8749,3763],[8766.5,3771],[8777.5,3784]],[[8800,3786.5],[8838,3805.5],[8833.5,3811]],[[8833,3829.5],[8852,3835],[8862.5,3847],[8841,3846.5]],[[8857,3807.5],[8864,3807],[8880,3842],[8862,3826]],[[8877.5,3853.5],[8899,3863.5],[8896,3874.5]],[[8762.5,3795.5],[8777.5,3799.5],[8793,3819.5],[8771.5,3811]]],"sd":[[[5243,3298],[5215,3246],[5247,3170],[5271,3170],[5271,3070],[5271,3058],[5299,3058],[5299,3002],[5511,3002],[5531,3010],[5575,2970],[5607,2998],[5623,3090],[5651,3110],[5611,3138],[5599,3210],[5555,3306],[5543,3302],[5531,3366],[5511,3366],[5507,3382],[5579,3446],[5583,3466],[5543,3466],[5531,3478],[5519,3490],[5451,3494],[5359,3454]]],"se":[[[4931,1826],[4979,1922],[4967,1934],[4983,1966],[5015,1958],[5023,1938],[5063,1934],[5083,1842],[5139,1798],[5111,1766],[5095,1758],[5091,1722],[5127,1666],[5207,1606],[5195,1590],[5227,1546],[5271,1550],[5259,1454],[5183,1406],[5167,1406],[5163,1438],[5115,1430],[5107,1454],[5095,1450],[5063,1478],[5067,1494],[5035,1534],[5019,1534],[4999,1602],[5011,1606],[5007,1626],[4979,1622],[4955,1658],[4959,1718],[4979,1734],[4963,1750],[4967,1786],[4951,1794],[4947,1826]]],"sg":[[[7363,3550],[7359,3554],[7367,3558],[7371,3550]]],"si":[[[4995,2294],[4999,2270],[5023,2274],[5067,2258],[5071,2270],[5051,2286],[5043,2306],[4999,2306]]],"sk":[[[5135,2170],[5231,2186],[5223,2210],[5179,2206],[5135,2222],[5135,2226],[5103,2230],[5087,2222],[5087,2198]]],"sl":[[[4339,3406],[4371,3362],[4359,3366],[4363,3358],[4347,3326],[4315,3326],[4291,3350],[4295,3370],[4311,3390]]],"sn":[[[4199,3262],[4239,3254],[4279,3254],[4339,3258],[4319,3202],[4263,3146],[4207,3158],[4203,3174],[4183,3198],[4203,3230],[4247,3222],[4279,3234],[4267,3238],[4243,3230],[4199,3242]]],"so":[[[5739,3482],[5795,3458],[5823,3458],[5899,3378],[5871,3378],[5791,3346],[5759,3306],[5767,3298],[5775,3286],[5803,3314],[5819,3314],[5979,3274],[5987,3310],[5927,3418],[5899,3470],[5847,3526],[5779,3574],[5731,3630],[5715,3610],[5715,3514]]],"sr":[[[3139,3434],[3115,3478],[3155,3538],[3207,3526],[3223,3494],[3207,3458],[3223,3434]]],"sv":[[[2291,3206],[2335,3222],[2331,3238],[2271,3222]]],"sy":[[[5583,2602],[5595,2602],[5603,2578],[5659,2578],[5755,2566],[5735,2586],[5727,2586],[5715,2650],[5659,2678],[5607,2710],[5575,2698],[5575,2682],[5599,2654],[5591,2642],[5583,2642]]],"sz":[[[5479,4286],[5463,4278],[5447,4298],[5459,4322],[5479,4322],[5483,4310]]],"td":[[[5035,2974],[5059,2962],[5271,3070],[5271,3170],[5247,3170],[5215,3246],[5243,3298],[5127,3374],[5047,3386],[5007,3330],[5015,3322],[5047,3322],[5035,3294],[5039,3270],[5019,3242],[5011,3242],[4995,3226],[4995,3206],[5047,3138],[5059,3046]]],"tf":[[[6451,4978],[6471,4994],[6491,4990],[6483,5014],[6447,5010]]],"tg":[[[4687,3422],[4679,3326],[4659,3314],[4663,3298],[4643,3294],[4635,3306],[4651,3318],[4655,3410],[4671,3426]]],"th":[[[7339,3274],[7327,3234],[7351,3210],[7403,3210],[7411,3206],[7411,3190],[7415,3174],[7391,3150],[7391,3130],[7355,3098],[7335,3118],[7323,3106],[7295,3126],[7291,3122],[7299,3070],[7279,3070],[7279,3054],[7267,3046],[7215,3066],[7199,3098],[7207,3118],[7235,3154],[7227,3166],[7227,3182],[7219,3186],[7227,3210],[7247,3226],[7243,3242],[7255,3278],[7227,3326],[7223,3386],[7231,3370],[7271,3422],[7275,3418],[7295,3426],[7295,3442],[7315,3438],[7323,3426],[7303,3406],[7291,3410],[7279,3398],[7263,3346],[7247,3346],[7243,3322],[7267,3270],[7267,3234],[7291,3234],[7287,3254],[7319,3258]]],"tj":[[[6459,2566],[6419,2566],[6435,2538],[6431,2514],[6407,2502],[6415,2490],[6439,2490],[6491,2446],[6499,2454],[6487,2466],[6503,2474],[6495,2482],[6479,2474],[6459,2486],[6463,2494],[6499,2498],[6575,2494],[6583,2522],[6603,2526],[6607,2566],[6591,2558],[6555,2562],[6523,2582],[6515,2566],[6519,2542],[6511,2542],[6511,2530],[6499,2526]]],"tl":[[[7919,3826],[7927,3818],[7979,3810],[7983,3814],[7923,3838]]],"tm":[[[6055,2562],[6123,2530],[6247,2582],[6251,2614],[6283,2626],[6367,2554],[6387,2562],[6387,2542],[6279,2478],[6255,2438],[6219,2434],[6215,2406],[6171,2394],[6135,2422],[6139,2438],[6111,2438],[6099,2442],[6067,2406],[6035,2410],[6019,2426],[6027,2438],[6027,2422],[6035,2414],[6055,2414],[6075,2446],[6051,2462],[6035,2454],[6027,2442],[6023,2478],[6043,2482],[6043,2494],[6035,2498],[6047,2502]]],"tn":[[[4943,2686],[4943,2706],[4903,2734],[4911,2754],[4887,2770],[4879,2718],[4835,2666],[4859,2626],[4863,2574],[4899,2562],[4915,2578],[4931,2570],[4915,2598],[4931,2626],[4903,2658]]],"to":[[[9489.33,4149.67],[9498.33,4151.33],[9493.5,4155.17]]],"tr":[[[5375,2418],[5331,2426],[5327,2458],[5343,2462],[5367,2446],[5403,2450],[5411,2458],[5367,2470],[5355,2466],[5339,2466],[5327,2482],[5327,2494],[5343,2502],[5347,2530],[5335,2530],[5367,2562],[5363,2570],[5423,2598],[5439,2590],[5443,2574],[5483,2586],[5499,2602],[5527,2598],[5555,2578],[5571,2586],[5583,2574],[5591,2582],[5579,2590],[5583,2602],[5595,2602],[5603,2578],[5659,2578],[5755,2566],[5815,2566],[5799,2498],[5811,2486],[5787,2478],[5783,2442],[5759,2430],[5731,2430],[5695,2450],[5647,2450],[5595,2442],[5559,2414],[5515,2418],[5463,2438],[5459,2446],[5403,2442],[5379,2430]]],"tt":[[[3016.38,3320.62],[3025,3316.62],[3021,3304.75],[3039,3301.88],[3036.25,3321]],[[3041.88,3292.5],[3048.88,3288.12],[3049.62,3291.38]]],"tw":[[[7819,2918],[7843,2918],[7815,3006],[7791,2978],[7795,2954]]],"tz":[[[5671,3710],[5627,3678],[5631,3670],[5534.67,3613.83],[5515.17,3637.67],[5527,3646],[5519,3654],[5475,3654],[5475,3614],[5439,3614],[5447,3650],[5451,3674],[5419,3706],[5427,3742],[5423,3754],[5435,3762],[5459,3814],[5503,3830],[5535,3842],[5559,3894],[5623,3894],[5703,3866],[5683,3850],[5671,3806],[5679,3770],[5659,3746]]],"ua":[[[5475,2082],[5455,2082],[5443,2106],[5303,2086],[5259,2098],[5271,2138],[5235,2170],[5239,2186],[5231,2186],[5223,2210],[5239,2222],[5295,2234],[5343,2210],[5363,2206],[5403,2222],[5427,2270],[5439,2282],[5447,2270],[5523,2290],[5495,2306],[5519,2314],[5519,2334],[5535,2338],[5595,2318],[5599,2306],[5571,2310],[5547,2294],[5567,2274],[5643,2250],[5651,2230],[5683,2226],[5691,2170],[5623,2138],[5603,2146],[5575,2142],[5559,2110],[5539,2110],[5527,2070]]],"ug":[[[5451,3494],[5519,3490],[5531,3478],[5563,3534],[5531,3582],[5515,3578],[5479,3590],[5475,3614],[5439,3614],[5427,3626],[5419,3622],[5419,3586],[5463,3530],[5447,3522]]],"us":[[[1563,2702],[1627,2702],[1723,2738],[1799,2738],[1799,2726],[1843,2726],[1931,2806],[1951,2778],[1975,2782],[2039,2878],[2087,2890],[2079,2850],[2159,2786],[2235,2786],[2259,2806],[2307,2766],[2379,2766],[2403,2786],[2431,2770],[2471,2810],[2463,2834],[2507,2910],[2531,2910],[2539,2870],[2499,2758],[2511,2722],[2655,2606],[2635,2542],[2647,2554],[2695,2486],[2699,2462],[2791,2422],[2779,2390],[2795,2358],[2883,2322],[2859,2294],[2859,2250],[2823,2238],[2759,2318],[2671,2318],[2643,2338],[2635,2366],[2623,2374],[2563,2374],[2567,2386],[2559,2398],[2491,2430],[2459,2430],[2451,2418],[2475,2382],[2463,2350],[2439,2362],[2451,2314],[2415,2294],[2375,2330],[2367,2366],[2375,2386],[2359,2422],[2343,2426],[2331,2378],[2363,2294],[2399,2282],[2411,2286],[2443,2286],[2419,2270],[2407,2262],[2351,2274],[2319,2250],[2323,2238],[2295,2254],[2223,2262],[2283,2218],[2143,2186],[1415,2186],[1415,2214],[1363,2210],[1383,2282],[1383,2334],[1367,2390],[1383,2438],[1371,2466],[1475,2642],[1539,2666]],[[1215,1986],[1227,1938],[1079,1798],[1031,1830],[935,1770],[935,1374],[523,1298],[275,1410],[267,1434],[411,1522],[391,1538],[335,1534],[335,1510],[227,1554],[279,1598],[363,1598],[407,1586],[415,1634],[351,1666],[323,1654],[279,1718],[307,1750],[291,1766],[347,1798],[375,1782],[391,1806],[387,1838],[431,1826],[463,1846],[515,1830],[499,1882],[307,1990],[319,1998],[455,1942],[615,1830],[591,1818],[667,1742],[651,1818],[755,1786],[759,1754],[787,1750],[863,1786],[955,1798],[963,1794],[971,1806],[1047,1854],[1103,1930],[1115,1914],[1155,1986],[1179,1978]]],"uy":[[[3235,4510],[3231,4494],[3243,4478],[3147,4402],[3127,4406],[3103,4514],[3163,4542],[3219,4538]]],"uz":[[[6419,2566],[6387,2562],[6387,2542],[6279,2478],[6255,2438],[6219,2434],[6215,2406],[6171,2394],[6135,2422],[6139,2438],[6111,2438],[6111,2322],[6179,2302],[6243,2342],[6267,2370],[6343,2362],[6367,2390],[6375,2386],[6375,2418],[6387,2418],[6395,2442],[6419,2442],[6439,2458],[6451,2438],[6503,2410],[6511,2414],[6487,2430],[6515,2442],[6523,2434],[6559,2454],[6523,2478],[6503,2474],[6487,2466],[6499,2454],[6491,2446],[6439,2490],[6415,2490],[6407,2502],[6431,2514],[6435,2538]]],"vc":[[[3031.88,3234.62],[3034.5,3236.38],[3032.38,3242.25],[3029.5,3238.75]]],"ve":[[[2751,3282],[2711,3346],[2759,3402],[2799,3402],[2815,3426],[2867,3422],[2855,3470],[2871,3498],[2859,3510],[2875,3526],[2883,3558],[2907,3570],[2975,3526],[2959,3522],[2939,3478],[2987,3494],[3047,3458],[3047,3450],[3027,3430],[3055,3402],[3043,3390],[3071,3366],[3043,3358],[3039,3338],[2991,3306],[2935,3322],[2903,3306],[2855,3310],[2819,3282],[2763,3302],[2775,3338],[2759,3346],[2743,3330],[2759,3306]]],"vn":[[[7475,3018],[7443,3002],[7443,2982],[7407,2966],[7347,2990],[7331,2982],[7323,2990],[7343,3010],[7343,3022],[7363,3038],[7375,3030],[7399,3054],[7371,3074],[7403,3094],[7399,3098],[7463,3166],[7455,3170],[7467,3186],[7463,3202],[7463,3262],[7419,3282],[7427,3306],[7415,3298],[7383,3314],[7399,3326],[7391,3366],[7503,3290],[7515,3250],[7499,3186],[7411,3086],[7423,3058]]],"vu":[[[9015.5,3976],[9019.5,4002.5],[9032.5,4000]],[[9031.5,4008.5],[9038,4026.5],[9047,4022.5]]],"wa":[[[9556.33,3944.17],[9568.17,3942.33],[9571.17,3951.83],[9563.17,3951.83]],[[9575.33,3953.83],[9582.17,3952.17],[9590.83,3959.33]]],"wf":[[[9415.75,3964],[9420.25,3967.5],[9417,3967.5]]],"ye":[[[5763,3154],[5775,3146],[5775,3130],[5783,3126],[5867,3134],[5879,3142],[5903,3110],[6003,3086],[6035,3150],[6011,3162],[6011,3174],[5787,3258]]],"za":[[[5503,4310],[5483,4310],[5479,4322],[5459,4322],[5447,4298],[5463,4278],[5479,4286],[5463,4186],[5411,4178],[5347,4222],[5311,4278],[5247,4266],[5211,4310],[5183,4310],[5179,4298],[5187,4290],[5163,4254],[5163,4354],[5147,4370],[5099,4362],[5087,4342],[5071,4362],[5123,4474],[5111,4482],[5163,4542],[5215,4526],[5315,4518],[5423,4442],[5491,4358]]],"zm":[[[5255,4058],[5215,4014],[5215,3930],[5271,3930],[5271,3878],[5279,3878],[5283,3890],[5303,3886],[5307,3894],[5347,3906],[5351,3894],[5407,3942],[5423,3942],[5423,3910],[5403,3914],[5383,3890],[5391,3870],[5387,3834],[5399,3814],[5443,3802],[5439,3810],[5455,3818],[5459,3814],[5503,3830],[5523,3866],[5511,3958],[5435,3986],[5439,4002],[5399,4010],[5395,4026],[5351,4062],[5299,4058],[5279,4050]]],"zw":[[[5463,4186],[5503,4126],[5503,4030],[5439,4010],[5439,4002],[5399,4010],[5395,4026],[5351,4062],[5299,4058],[5375,4162],[5411,4178]]]}
2  python.requirements
@@ -0,0 +1,2 @@
+erlport >= 0.6
+pygeoip >= 0.2.2
8 rebar.config
@@ -0,0 +1,8 @@
+%%-*- mode: erlang -*-
+
+{deps, [{webmachine, "1.9.*", {git, "git://github.com/basho/webmachine", "HEAD"}},
+ {erlydtl, "0.7.*", {git, "https://github.com/evanmiller/erlydtl", "HEAD"}}]}.
+{cover_enabled, true}.
+{clean_files, ["*.eunit", "ebin/*.beam"]}.
+{eunit_opts, [verbose]}.
+{erl_opts, [report]}.
26 src/erli.app.src
@@ -0,0 +1,26 @@
+%%-*- mode: erlang -*-
+{application, erli,
+ [
+ {description, "erli - an Erlang URL shortener"},
+ {vsn, "0.1.1"},
+ {modules, [erli,
+ erli_app,
+ erli_error_handler,
+ erli_stats,
+ erli_storage,
+ erli_sup,
+ erli_util,
+ path_resource,
+ root_resource,
+ static_resource]},
+ {registered, [erli_sup]},
+ {applications, [kernel,
+ stdlib,
+ inets,
+ crypto,
+ mochiweb,
+ webmachine,
+ mnesia]},
+ {mod, {erli_app, []}},
+ {env, []}
+ ]}.
54 src/erli.erl
@@ -0,0 +1,54 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011-2012 Moritz Windelen.
+
+%% @doc erli startup code
+
+-module(erli).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+-export([start/0, start_link/0, stop/0]).
+
+ensure_started(App) ->
+ case application:start(App) of
+ ok ->
+ ok;
+ {error, {already_started, App}} ->
+ ok
+ end.
+
+%% @spec start_link() -> {ok,Pid::pid()}
+%% @doc Starts the app for inclusion in a supervisor tree
+start_link() ->
+ ensure_started(inets),
+ ensure_started(crypto),
+ ensure_started(mochiweb),
+ ensure_started(mnesia),
+ erli_storage:init({}), % initialize mnesia
+ application:set_env(webmachine, webmachine_logger_module,
+ webmachine_logger),
+ ensure_started(webmachine),
+ erli_sup:start_link().
+
+%% @spec start() -> ok
+%% @doc Start the erli server.
+start() ->
+ ensure_started(inets),
+ ensure_started(crypto),
+ ensure_started(mochiweb),
+ ensure_started(mnesia),
+ erli_storage:init({}), % initialize mnesia
+ application:set_env(webmachine, webmachine_logger_module,
+ webmachine_logger),
+ ensure_started(webmachine),
+ application:start(erli).
+
+%% @spec stop() -> ok
+%% @doc Stop the erli server.
+stop() ->
+ Res = application:stop(erli),
+ application:stop(webmachine),
+ application:stop(mochiweb),
+ application:stop(crypto),
+ application:stop(inets),
+ application:stop(mnesia),
+ Res.
15 src/erli.hrl
@@ -0,0 +1,15 @@
+-define(FLAG_LIMIT, 5). % number of reports before Target URLs are banned
+
+-define(STAT_COLLECT_INTERVAL, 6000). % this breaks statistics... hardcore
+-define(MAX_CONFLICTS, 5).
+%-define(STAT_COLLECT_INTERVAL, 3600000). % time in ms between calls to the parser script
+%-define(MAX_CONFLICTS, 100). % the maximal number of attempts to generate a new URL path
+
+-define(SCRIPT_NAME, "parse_eval.py").
+
+-record(target, {target, paths=[], reported = 0, rep_num = 0}).
+-record(path, {path, total_clicks = 0, unique_clicks = 0, country_lst = []}).
+-record(visitor_ip,{visitor_ip, paths=[]}).
+
+
+
21 src/erli_app.erl
@@ -0,0 +1,21 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright Moritz Windelen 2011.
+
+%% @doc Callbacks for the erli application.
+
+-module(erli_app).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+-behaviour(application).
+-export([start/2,stop/1]).
+
+
+%% @spec start(_Type, _StartArgs) -> ServerRet
+%% @doc application start callback for erli.
+start(_Type, _StartArgs) ->
+ erli_sup:start_link().
+
+%% @spec stop(_State) -> ServerRet
+%% @doc application stop callback for erli.
+stop(_State) ->
+ ok.
71 src/erli_error_handler.erl
@@ -0,0 +1,71 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+
+%% @doc erlydtl based error handlers.
+
+-module(erli_error_handler).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+-export([render_error/3]).
+
+render_error(Code, Req, Reason) ->
+ case Req:has_response_body() of
+ {true,_} ->
+ Req:response_body();
+ {false,_} ->
+ render_error_body(Code, Req:trim_state(), Reason)
+ end.
+
+%%%=============================================================================
+%%% @doc HTTP status code handlers.
+%%% Currently implements: 400, 403, 404, 500, 501, 502, 503
+%%% @end
+%%%=============================================================================
+render_error_body(400, Req, _Reason) ->
+ {ok, ReqState} = Req:add_response_header("Content-Type", "text/html"),
+ {ok, Content} = badrequest_dtl:render(),
+ {Content, ReqState};
+
+render_error_body(403, Req, _Reason) ->
+ {ok, ReqState} = Req:add_response_header("Content-Type", "text/html"),
+ {ok, Content} = badrequest_dtl:render(),
+ {Content, ReqState};
+
+render_error_body(404, Req, _Reason) ->
+ {ok, ReqState} = Req:add_response_header("Content-Type", "text/html"),
+ {ok, Content} = notfound_dtl:render(),
+ {Content, ReqState};
+
+render_error_body(500, Req, Reason) ->
+ {ok, ReqState} = Req:add_response_header("Content-Type", "text/html"),
+ {Path,_} = Req:path(),
+ case Reason of
+ {error, {exit, normal, _Stack}} ->
+ ok;
+ _ ->
+ error_logger:error_msg("[WEBMACHINE] Internal Error: path=~p~n~p~n", [Path, Reason])
+ end,
+ {ok, Content} = serverfault_dtl:render(),
+ {Content, ReqState};
+
+render_error_body(501, Req, _Reason) ->
+ {ok, ReqState} = Req:add_response_header("Content-Type", "text/html"),
+ {Method,_} = Req:method(),
+ error_logger:error_msg("[WEBMACHINE] Method not supported: ~p~n",
+ [Method]),
+ {ok, Content} = notimplemented_dtl:render(),
+ {Content, ReqState};
+
+render_error_body(502, Req, _Reason) ->
+ {ok, ReqState} = Req:add_response_header("Content-Type", "text/html"),
+ error_logger:error_msg("[WEBMACHINE] Bad Gateway: ~p~n",
+ [Req]),
+ {ok, Content} = badgateway_dtl:render(),
+ {Content, ReqState};
+
+render_error_body(503, Req, _Reason) ->
+ {ok, ReqState} = Req:add_response_header("Content-Type", "text/html"),
+ error_logger:error_msg("[WEBMACHINE] Cannot fulfill request: ~p~n",
+ [Req]),
+ {ok, Content} = unavailable_dtl:render(),
+ {Content, ReqState}.
85 src/erli_stats.erl
@@ -0,0 +1,85 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+
+%% @doc gen_server that handles periodic parsing of
+%% webmachine logfiles to extract usage statistics
+%% @end
+
+-module(erli_stats).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0]).
+
+%% gen_server callbacks
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+-include("erli.hrl").
+
+start_link() ->
+ gen_server:start_link(?MODULE, [], []).
+
+%%%=============================================================================
+%%% gen_server callbacks
+%%%=============================================================================
+init([]) ->
+ {ok, {}, 0}.
+
+handle_call(_Req, _From, State) ->
+ {reply, ok, State}.
+
+handle_cast(_Req, State) ->
+ {noreply, State}.
+
+handle_info(parse_eval, State) ->
+ {ok, App} = application:get_application(),
+ PyCmd = "python -u " ++ filename:join([code:priv_dir(App), "scripts", ?SCRIPT_NAME]),
+ Port = open_port({spawn, PyCmd}, [{packet, 1}, binary, use_stdio]),
+ case grab_path_stats(Port, erli_storage:path_list()) of
+ true ->
+ erlang:send_after(?STAT_COLLECT_INTERVAL, self(), parse_eval);
+ {reschedule, Time} ->
+ erlang:send_after(Time, self(), parse_eval)
+ end,
+ {noreply, State};
+handle_info(timeout, State) ->
+ erlang:send_after(0, self(), parse_eval), % triggers the initial parsing cycle
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%=============================================================================
+%%% Internal functions
+%%%=============================================================================
+grab_path_stats(Port, [Path | RemPaths]) ->
+ port_command(Port, term_to_binary({path, Path#path.path})),
+ receive
+ {Port, {data, RespData}} ->
+ case binary_to_term(RespData) of
+ {reschedule, Time} ->
+ grab_path_stats(Port, []),
+ {reschedule, Time};
+ {Countries, UniqueIPs, ClickCount} ->
+ erli_storage:update_path_stats(Path, Countries, UniqueIPs, ClickCount),
+ grab_path_stats(Port, RemPaths)
+ end
+ after
+ 5000 ->
+ error_logger:warning_msg("[ERLI] ~s timed out on grab_path_stats/2"
+ " for path ~s~n",
+ [?MODULE, Path]),
+ {error, timeout}
+ end;
+grab_path_stats(Port, []) ->
+ port_close(Port).
278 src/erli_storage.erl
@@ -0,0 +1,278 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+%% @doc The API to the database backed storage of URLs
+
+-module(erli_storage).
+
+%% API
+-export([init/1,
+ put/1,
+ put/2,
+ read/1,
+ delete/1,
+ path_list/0,
+ update_path_stats/4]).
+
+-include_lib("stdlib/include/qlc.hrl").
+-include("erli.hrl").
+
+%%------------------------------------------------------------------------------
+%% @doc Initializes the mnesia backend, includes setting up necessary tables, etc.
+%%------------------------------------------------------------------------------
+init(_Config) ->
+ case is_fresh_startup(node()) of
+ true ->
+ mnesia:stop(),
+ mnesia:create_schema([node()]),
+ mnesia:start(),
+ create_tables();
+ {exists, Tables} ->
+ ok = mnesia:wait_for_tables(Tables, 60000)
+ end.
+
+%%------------------------------------------------------------------------------
+%% @doc Creates a new entry for the target URL or returns the current shortened
+%% path for the URL.
+%% @spec put(Target :: binary) -> {ok, #target} | {target_banned, #target}| error
+%% @end
+%%------------------------------------------------------------------------------
+put(TargetUrl) ->
+ {atomic, MatchingTarget} = mnesia:transaction(
+ fun() ->
+ Table = mnesia:table(target),
+ QueryHandle = qlc:q([T || T <- Table,
+ T#target.target =:= binary_to_list(TargetUrl)]),
+ qlc:eval(QueryHandle)
+ end),
+ make_target(TargetUrl, MatchingTarget).
+
+put(TargetUrl, PathS) ->
+ Path = #path{path=PathS},
+ case read(PathS) of
+ not_found ->
+ {atomic, MatchingTarget} =
+ mnesia:transaction(
+ fun() ->
+ Table = mnesia:table(target),
+ QueryHandle = qlc:q([T || T <- Table,
+ T#target.target =:= binary_to_list(TargetUrl)]),
+ qlc:eval(QueryHandle)
+ end),
+ case MatchingTarget of
+ [T] when T#target.rep_num > ?FLAG_LIMIT ->
+ target_banned;
+ [#target{paths=[ExistingPaths], _=_} = T] ->
+ NewTarget = T#target{paths=[Path|ExistingPaths]},
+ {atomic, _Ret} = mnesia:transaction(
+ fun() ->
+ mnesia:write(target,
+ NewTarget,
+ write)
+ end),
+ {ok, NewTarget};
+ [] ->
+ NewTarget = #target{target=TargetUrl, paths=[Path]},
+ {atomic, _Ret} = mnesia:transaction(
+ fun() ->
+ mnesia:write(target,
+ NewTarget,
+ write)
+ end),
+ {ok, NewTarget}
+ end;
+ _ ->
+ conflict
+ end.
+
+
+%%------------------------------------------------------------------------------
+%% @doc Retrieves the complete short_url record for a shortened URL.
+%% @spec read(Path :: Binary) -> {ok, #target} | {target_banned, #target} | not_found
+%% @end
+%%------------------------------------------------------------------------------
+read(PathS) ->
+ {atomic, Result} = mnesia:transaction(
+ fun() ->
+ Table = mnesia:table(target),
+ QueryHandle = qlc:q([T || T <- Table, path_in_target(T#target.paths,
+ PathS)]),
+ qlc:eval(QueryHandle)
+ end),
+ case Result of
+ [T] when T#target.rep_num > ?FLAG_LIMIT ->
+ {target_banned, T};
+ [T] ->
+ {ok, T};
+ _ ->
+ not_found
+ end.
+
+%%------------------------------------------------------------------------------
+%% @doc Flags the URL linked by Path.
+%% @spec delete(Path :: Binary) -> {ok, #target} | not_found
+%% @todo implement
+%% @end
+%%------------------------------------------------------------------------------
+delete(PathS) ->
+ case read(PathS) of
+ {_, #target{rep_num = RepNum, _=_} = Target} ->
+ NewTarget = Target#target{reported=true,
+ rep_num=RepNum+1},
+ ok = mnesia:dirty_write(target, NewTarget),
+ {ok, NewTarget};
+ not_found ->
+ not_found
+ end.
+
+%%------------------------------------------------------------------------------
+%% @doc Returns a list of all current paths.
+%% @end
+%%------------------------------------------------------------------------------
+path_list() ->
+ PathRecs = mnesia:dirty_select(target, [{#target{paths='$1', _='_'}, [], ['$1']}]),
+ lists:flatten(PathRecs).
+
+%%------------------------------------------------------------------------------
+%% @doc Update a shortened URL's visit statistics
+%% @end
+%%------------------------------------------------------------------------------
+update_path_stats(Path, Countries, UniqueIPs, ClickCount) ->
+ % grab the target record
+ {ok, #target{paths=Paths, _=_} = Target} = read(Path#path.path),
+ % filter out the important path (not doing this in the read call leaves it 'cheap'
+ {[#path{country_lst=CL, total_clicks=TC, unique_clicks=UC, _=_}=ThePath],
+ OtherPaths} =
+ lists:partition(fun(P) ->
+ Path =:= P
+ end,
+ Paths),
+
+ % check if there is a new ip for this path
+ UniqueClicks = length([IP || IP <- UniqueIPs, is_unique_for_path(Path, IP)]),
+
+ % merge old and new country lists
+ CountryUnion = sets:to_list(
+ sets:union(
+ sets:from_list(CL),
+ sets:from_list(Countries)
+ )
+ ),
+ % update the path stats
+ NewPath = ThePath#path{country_lst = CountryUnion,
+ unique_clicks = UC + UniqueClicks,
+ total_clicks = TC + ClickCount},
+ NewTarget = Target#target{paths = [NewPath | OtherPaths]},
+ % flush to mnesia
+ mnesia:dirty_write(NewTarget).
+
+%%%=============================================================================
+%%% Internal functions
+%%%=============================================================================
+is_unique_for_path(Path, IP) ->
+ case mnesia:dirty_read(visitor_ip, #visitor_ip{visitor_ip=IP, _='_'}) of
+ [] ->
+ mnesia:dirty_write(visitor_ip, #visitor_ip{visitor_ip=IP,
+ paths=[Path]}),
+ true;
+ [VisitedPaths] ->
+ case lists:member(Path, VisitedPaths) of
+ true ->
+ false;
+ false ->
+ mnesia:dirty_write(visitor_ip, #visitor_ip{visitor_ip=IP,
+ paths=[Path|VisitedPaths]}),
+ true
+ end
+ end.
+
+path_in_target(TargetPaths, SearchPath) ->
+ Path = lists:filter(
+ fun(#path{path=ThePath, _=_}) -> ThePath =:= SearchPath end,
+ TargetPaths),
+ case Path of
+ [] ->
+ false;
+ _ ->
+ true
+ end.
+
+make_target(TargetUrl, MatchingTarget) ->
+ case MatchingTarget of
+ [T] when T#target.rep_num > ?FLAG_LIMIT ->
+ target_banned;
+ [#target{paths=[ExistingPaths], _=_} = T] ->
+ case make_unique_path(TargetUrl) of
+ error ->
+ error;
+ PathS ->
+ Path = #path{path=PathS},
+ NewTarget = T#target{paths=[Path|ExistingPaths]},
+ {atomic, _Ret} = mnesia:transaction(
+ fun() ->
+ mnesia:write(target,
+ NewTarget,
+ write)
+ end),
+ {ok, Path}
+ end;
+ [] ->
+ case make_unique_path(TargetUrl) of
+ error ->
+ error;
+ PathS ->
+ Path = #path{path=PathS},
+ T = #target{target=TargetUrl, paths=[Path]},
+ {atomic, _Ret} = mnesia:transaction(
+ fun() ->
+ mnesia:write(target,
+ T,
+ write)
+ end),
+ {ok, Path}
+ end
+ end.
+
+%%------------------------------------------------------------------------------
+%% @doc Generate a unique path using the base64 encoded md5 of the current time +
+%% the target URL.
+%% @end
+%%------------------------------------------------------------------------------
+
+make_unique_path(TargetUrl) ->
+ make_unique_path(TargetUrl, 0).
+make_unique_path(TargetUrl, NrOfHashConflicts) ->
+ {MegaSec, Sec, MiniSec} = erlang:now(),
+ B = <<TargetUrl/bytes, MegaSec, Sec, MiniSec>>,
+ Base64Path = string:to_lower(binary_to_list(base64:encode(crypto:md5(B)), 1, 5)),
+ CleanPath = re:replace(Base64Path, "[/?=]","+", [{return, list}, global]),
+ case mnesia:dirty_read(target, CleanPath) of
+ [] ->
+ CleanPath;
+ _Res ->
+ if
+ NrOfHashConflicts < ?MAX_CONFLICTS ->
+ make_unique_path(TargetUrl, NrOfHashConflicts + 1);
+ true ->
+ error
+ end
+ end.
+
+% modified from: http://erlang.2086793.n4.nabble.com/When-to-create-mnesia-schema-for-OTP-applications-td2115607.html
+is_fresh_startup(Node) ->
+ case mnesia:system_info(tables) of
+ [schema] ->
+ true;
+ Tbls ->
+ case mnesia:table_info(schema, cookie) of
+ {_, Node} ->
+ {exists, Tbls};
+ _ ->
+ true
+ end
+ end.
+
+create_tables() ->
+ mnesia:create_table(target, [{attributes, record_info(fields, target)},
+ {disc_copies, [node()]}]),
+ mnesia:create_table(visitor_ip, [{attributes, record_info(fields, visitor_ip)},
+ {disc_copies, [node()]}]).
60 src/erli_sup.erl
@@ -0,0 +1,60 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+
+%% @doc Supervisor for the erli application.
+
+-module(erli_sup).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+-behaviour(supervisor).
+
+%% External exports
+-export([start_link/0, upgrade/0]).
+
+%% supervisor callbacks
+-export([init/1]).
+
+%% @spec start_link() -> ServerRet
+%% @doc API for starting the supervisor.
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% @spec upgrade() -> ok
+%% @doc Add processes if necessary.
+upgrade() ->
+ {ok, {_, Specs}} = init([]),
+
+ Old = sets:from_list(
+ [Name || {Name, _, _, _} <- supervisor:which_children(?MODULE)]),
+ New = sets:from_list([Name || {Name, _, _, _, _, _} <- Specs]),
+ Kill = sets:subtract(Old, New),
+
+ sets:fold(fun (Id, ok) ->
+ supervisor:terminate_child(?MODULE, Id),
+ supervisor:delete_child(?MODULE, Id),
+ ok
+ end, ok, Kill),
+
+ [supervisor:start_child(?MODULE, Spec) || Spec <- Specs],
+ ok.
+
+%% @spec init([]) -> SupervisorTree
+%% @doc supervisor callback.
+init([]) ->
+ Ip = case os:getenv("WEBMACHINE_IP") of false -> "0.0.0.0"; Any -> Any end,
+ {ok, Dispatch} = file:consult(filename:join(
+ [filename:dirname(code:which(?MODULE)),
+ "..", "priv", "dispatch.conf"])),
+ WebConfig = [
+ {ip, Ip},
+ {port, 8000},
+ {log_dir, "priv/log"},
+ {dispatch, Dispatch},
+ {error_handler, erli_error_handler}],
+ Web = {webmachine_mochiweb,
+ {webmachine_mochiweb, start, [WebConfig]},
+ permanent, 5000, worker, [mochiweb_socket_server]},
+ ParseEval = {erli_stats, {erli_stats, start_link, []},
+ permanent, 5000, worker, [erli_stats]},
+ Processes = [Web, ParseEval],
+ {ok, { {one_for_one, 10, 10}, Processes} }.
23 src/erli_util.erl
@@ -0,0 +1,23 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+%% @doc erli utility methods.
+
+%% TODO: make this a gen_server
+
+-module(erli_util).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+-export([is_valid_url/1]).
+
+-include_lib("webmachine/include/webmachine.hrl").
+-include("erli.hrl").
+
+is_valid_url(Url) ->
+ {ok, Re} = re:compile("^(?:[a-zA-Z0-9]+:\/\/)(?:(?:(?:[a-zA-Z0-9]+\.)+(?:[a-zA-Z]+))|(?:(?:[0-9]+\.){3}(?:[0-9]+))|(?:(?:[a-f0-9]+\:)+(?:[a-f0-9]+)))(?:(?:\s*$)|(?:(?:[:\/?]).+$))", [dotall]),
+ case re:run(Url, Re, [{capture, none}]) of
+ match ->
+ true;
+ nomatch ->
+ false
+ end.
+
150 src/path_resource.erl
@@ -0,0 +1,150 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+%% @doc The erli resource for handling anything beyond "/".
+
+-module(path_resource).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+%% Webmachine resource function
+-export([init/1,
+ allowed_methods/2,
+ content_types_provided/2,
+ content_types_accepted/2,
+ delete_resource/2,
+ delete_completed/2,
+ resource_exists/2]).
+%% Accept handler functions
+-export([to_html/2,
+ to_json/2]).
+%% Content-Type handler functions
+-export([from_json/2]).
+
+-include_lib("webmachine/include/webmachine.hrl").
+-include("erli.hrl").
+
+init([]) ->
+ {{trace, "/tmp"}, #target{}}.
+% {ok, #target{}}.
+
+allowed_methods(RD, Ctx) ->
+ case wrq:disp_path(RD) of
+ "" ->
+ {['GET', 'DELETE', 'PUT'], RD, Ctx};
+ _ ->
+ {['GET'], RD, Ctx}
+ end.
+
+resource_exists(RD, Ctx) ->
+ case erli_storage:read(wrq:path_info(path, RD)) of
+ {ok, Target} ->
+ {true, RD, Target};
+ _ ->
+ {false, RD, Ctx}
+ end.
+
+delete_resource(RD, Ctx) ->
+ {ok, _Target} = erli_storage:delete(wrq:path_info(path, RD)),
+ {true, RD, Ctx}.
+
+delete_completed(RD, Ctx) ->
+ {false, RD, Ctx}. % erli only increments rep_num on a delete -> 202
+
+content_types_provided(RD, Ctx) ->
+ {[{"text/html", to_html}, {"application/json", to_json}], RD, Ctx}.
+
+to_html(RD, Ctx) ->
+ case wrq:disp_path(RD) of
+ "" ->
+ handle_path(html, RD, Ctx); % landing page || redir
+ _ ->
+ handle_path_subreq(html, RD, Ctx) % /path/stats || /path/report
+ end.
+
+to_json(RD, Ctx) ->
+ case wrq:disp_path(RD) of
+ "" ->
+ handle_path(json, RD, Ctx); % landing page || redir
+ _ ->
+ handle_path_subreq(json, RD, Ctx) % /path/stats || /path/report
+ end.
+
+handle_path(Type, RD, Ctx) ->
+ NRD = wrq:set_resp_header("Location", binary_to_list(Ctx#target.target), RD),
+ case wrq:get_qs_value("landing", RD) of
+ undefined -> {{halt, 302}, NRD, Ctx};
+ _ ->
+ case Type of
+ html ->
+ {ok, Content} = landing_dtl:render([{target, Ctx#target.target},
+ {path, wrq:path_info(path)}]),
+ {Content, NRD, Ctx};
+ json ->
+ Content = mochijson2:encode([{target, Ctx#target.target}]),
+ {Content, NRD, Ctx}
+ end
+ end.
+
+handle_path_subreq(Type, RD, Ctx) ->
+ Path = wrq:path_info(path, RD),
+ {[ThePath], _OtherPaths} =
+ lists:partition(fun(#path{path=P, _=_}=_Path) -> Path =:= P end,
+ Ctx#target.paths),
+ case wrq:disp_path(RD) of
+ "stats" ->
+ case Type of
+ html ->
+ {ok, Content} = stats_dtl:render([{target, Ctx#target.target},
+ {path, list_to_binary(ThePath#path.path)},
+ {total_clicks, ThePath#path.total_clicks},
+ {unique_clicks, ThePath#path.unique_clicks},
+ {country_lst, ThePath#path.country_lst}]),
+ {Content, RD, Ctx};
+ json ->
+ % TODO: switch from mochijson2 to https://github.com/davisp/eep0018
+ Content = mochijson2:encode({struct, [{target, Ctx#target.target},
+ {path, list_to_binary(ThePath#path.path)},
+ {total_clicks, ThePath#path.total_clicks},
+ {unique_clicks, ThePath#path.unique_clicks},
+ {country_lst, ThePath#path.country_lst}]}),
+ {Content, RD, Ctx}
+ end;
+ "report" ->
+ case Type of
+ html ->
+ {ok, Content} = report_dtl:render([{target, Ctx#target.target},
+ {path, list_to_binary(ThePath#path.path)}]),
+ {Content, RD, Ctx};
+ json ->
+ {{halt, 202}, RD, Ctx} % with a 202
+ end;
+ "check" ->
+ {{halt, 200}, RD, Ctx}; % fake landing for low overhead checking of path availability
+ _ ->
+ {{halt, 404}, RD, Ctx}
+ end.
+
+content_types_accepted(RD, Ctx) ->
+ NRD = wrq:set_max_recv_body(1024, RD), % no need to accept large bodies
+ {[{"application/json", from_json}], NRD, Ctx}.
+
+from_json(RD, Ctx) ->
+ case mochijson2:decode(wrq:req_body(RD)) of
+ {struct, [{<<"url">>, TargetUrl}, {<<"tou_checked">>, true}]} ->
+ case erli_util:is_valid_url(TargetUrl) of
+ false ->
+ % TODO: add request body which contains some kind of info (e.g. needs a schema definition)
+ {{halt, 400}, RD, Ctx};
+ true ->
+ case erli_storage:put(TargetUrl,
+ wrq:path_info(path, RD)) of
+ {ok, Target} ->
+ {true, RD, Target};
+ conflict ->
+ {{halt, 409}, RD, Ctx};
+ {target_banned, _Target} ->
+ {{halt, 410}, RD, Ctx}
+ end
+ end;
+ _ ->
+ {{halt, 400}, RD, Ctx}
+ end.
88 src/root_resource.erl
@@ -0,0 +1,88 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+%% @doc The erli root resource.
+
+-module(root_resource).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+%% Webmachine resource function
+-export([init/1,
+ allowed_methods/2,
+ content_types_provided/2,
+ post_is_create/2,
+ process_post/2]).
+%% Accept handler functions
+-export([to_html/2]).
+
+-include_lib("webmachine/include/webmachine.hrl").
+-include("erli.hrl").
+
+init([]) ->
+ {{trace, "/tmp"}, #target{}}. % debug mode
+% {ok, #target{}}.
+
+allowed_methods(RD, Ctx) ->
+ {['GET', 'POST'], RD, Ctx}.
+
+content_types_provided(RD, Ctx) ->
+ {[{"text/html", to_html}], RD, Ctx}.
+
+to_html(RD, Ctx) ->
+ {ok, Content} = index_dtl:render([]),
+ {Content, RD, Ctx}.
+
+post_is_create(RD, Ctx) ->
+ {false, RD, Ctx}.
+
+process_post(RD, Ctx) ->
+ NRD = wrq:set_max_recv_body(1024, RD), % no need to accept large bodies
+ case wrq:get_req_header("Content-Type", NRD) of
+ "application/json" ->
+ from_json(NRD, Ctx);
+ "application/x-www-form-urlencoded" ->
+ from_urlencoded(NRD, Ctx);
+ _ ->
+ {{halt, 415}, NRD, Ctx}
+ end.
+
+%%%=============================================================================
+%%% Internal functions
+%%%=============================================================================
+from_json(RD, Ctx) ->
+ case mochijson2:decode(wrq:req_body(RD)) of
+ {struct, [{<<"url">>, TargetUrl}, {<<"tou_checked">>, true}]} ->
+ maybe_store(RD, #target{target=TargetUrl});
+ _ ->
+ {{halt, 400}, RD, Ctx}
+ end.
+
+from_urlencoded(RD, Ctx) ->
+ case wrq:get_qs_value("tou_checked", RD) of
+ true ->
+ case wrq:get_qs_value("url", RD) of
+ undefined ->
+ {{halt, 400}, RD, Ctx}; % the duplication of the 400 here is 'ungood'
+ TargetUrl ->
+ maybe_store(RD, #target{target=TargetUrl})
+ end;
+ _ ->
+ {{halt, 400}, RD, Ctx}
+ end.
+
+maybe_store(RD, Ctx) ->
+ case erli_util:is_valid_url(Ctx#target.target) of
+ false ->
+ % TODO: add request body which contains some kind of info (e.g. needs a schema definition)
+ {{halt, 400}, RD, Ctx};
+ true ->
+ case erli_storage:put(Ctx#target.target) of
+ error ->
+ {error, RD, Ctx}; % storage errors return a 500
+ {target_banned, _Target} ->
+ {{halt, 410}, RD, Ctx}; % banned target urls return a 410
+ {ok, Path} ->
+ NRD = wrq:set_resp_header("Location", Path#path.path, RD),
+ {true, NRD, Ctx}
+ end
+ end.
+
77 src/static_resource.erl
@@ -0,0 +1,77 @@
+%% @author Moritz Windelen <moritz@tibidat.com>
+%% @copyright 2011 Moritz Windelen.
+%% @doc Simplistic static content resource.
+
+-module(static_resource).
+-author('Moritz Windelen <moritz@tibidat.com>').
+
+% Webmachine resource functions
+-export([init/1,
+ allowed_methods/2,
+ content_types_provided/2]).
+% Content-Type handlers
+-export([maybe_provide_content/2]).
+
+-include_lib("webmachine/include/webmachine.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+-record(state, {basedir, fpath}).
+
+
+init([StaticDir]) ->
+ {ok, App} = application:get_application(),
+ BaseDir = filename:join(code:priv_dir(App), StaticDir),
+ {ok, #state{basedir=BaseDir}}.
+
+allowed_methods(ReqData, Context) ->
+ {['GET'], ReqData, Context}.
+
+content_types_provided(ReqData, Context) ->
+ {[{webmachine_util:guess_mime(wrq:disp_path(ReqData)), maybe_provide_content}],
+ ReqData,
+ Context#state{fpath=determine_fpath(ReqData, Context)}}.
+
+%%%=============================================================================
+%%% Content Handler
+%%%=============================================================================
+%%------------------------------------------------------------------------------
+%% @doc Determines whether to provide the requested resource, based on whether
+%% it exists and is 'safe'.
+%% @end
+%%------------------------------------------------------------------------------
+maybe_provide_content(ReqData, #state{fpath=Path, _=_}=Context) ->
+ case Path of
+ undefined ->
+ {{halt, 403}, ReqData, Context};
+ P ->
+ FPath = filename:join(Context#state.basedir, P),
+ case filelib:is_regular(FPath) of
+ true ->
+ fetch_content(ReqData, Context#state{fpath=FPath});
+ false ->
+ {{halt, 404}, ReqData, Context}
+ end
+ end.
+
+%%%=============================================================================
+%%% Internal functions
+%%%=============================================================================
+%%------------------------------------------------------------------------------
+%% @doc Returns either a sanitized path for a resource or undefined.
+%% @end
+%%------------------------------------------------------------------------------
+determine_fpath(ReqData, Context) ->
+ case mochiweb_util:safe_relative_path(wrq:disp_path(ReqData)) of
+ undefined ->
+ undefined;
+ Path ->
+ filename:join(Context#state.basedir, Path)
+ end.
+
+%%------------------------------------------------------------------------------
+%% @doc Returns the resource contents.
+%% @end
+%%------------------------------------------------------------------------------
+fetch_content(ReqData, Context) ->
+ {ok, Content} = file:read_file(Context#state.fpath),
+ {Content, ReqData, Context}.
3  start.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd `dirname $0`
+exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader -s erli -sname erli_node -mnesia dir "\"$PWD/priv/mnesia.store\""
26 templates/base.dtl
@@ -0,0 +1,26 @@
+<!doctype html>
+<head>
+ <title>erli :: {% block title %}{% endblock %}</title>
+ <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css" type="text/css" media="screen" />
+ <link rel="stylesheet" href="/static/erli.css" type="text/css" media="screen" /> <!-- slight modifications of the original bootstrap css-->
+ <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js"></script>
+ <script type="text/javascript" src="http://twitter.github.com/bootstrap/1.4.0/bootstrap-twipsy.js"></script>
+ <script type="text/javascript" src="http://twitter.github.com/bootstrap/1.4.0/bootstrap-popover.js"></script>
+ <script type="text/javascript">
+ $(document).ready(function() {
+ $("#about_popover").popover();
+ });
+ </script>
+ {% block extra_js %}{% endblock %}
+</head>
+<body>
+ <div class="container">
+ <h1><a href="/">erli</a> <small>{% block heading %}{% endblock %}</small></h1>
+ {% block body %}{% endblock %}
+ </div>
+ <footer>
+ Created by Moritz Windelen 2012 <span id="about_popover" rel="popover" data-content="This is a weekend project of mine as a way of deepening my understanding of basic Erlang and to experiment the very cool Webmachine REST-kit." data-original-title="About erli">(why?)</span>
+ </footer>
+ {% block modal_content %}{% endblock %}
+</body>
+</html>
9 templates/error_handlers/badgateway.dtl
@@ -0,0 +1,9 @@
+{% extends "base.dtl" %}
+
+{% block title %}bad gateway{% endblock %}
+
+{% block heading %}bad gateway{% endblock %}
+
+{% block body %}
+<p>Something caused <strong>erli</strong> to receive a broken request.</p>
+{% endblock %}
9 templates/error_handlers/badrequest.dtl
@@ -0,0 +1,9 @@
+{% extends "base.dtl" %}
+
+{% block title %}bad request{% endblock %}
+
+{% block heading %}bad request{% endblock %}
+
+{% block body %}
+<p>You're asking <strong>erli</strong> to do something it's not supposed to...</p>
+{% endblock %}
8 templates/error_handlers/forbidden.dtl
@@ -0,0 +1,8 @@
+{% extends "base.dtl" %}
+
+{% block title %}forbidden{% endblock %}
+
+{% block heading %}forbidden{% endblock %}
+{% block body %}
+<p>Sorry, but what you're attempting to do is forbidden.</p>
+{% endblock %}
9 templates/error_handlers/notfound.dtl
@@ -0,0 +1,9 @@
+{% extends "base.dtl" %}
+
+{% block title %}resource not found{% endblock %}
+
+{% block heading %}resource not found{% endblock %}
+
+{% block body %}
+<p>The resource you were trying to reach doesn't exist.</p>
+{% endblock %}
9 templates/error_handlers/notimplemented.dtl
@@ -0,0 +1,9 @@
+{% extends "base.dtl" %}
+
+{% block title %}method not implemented{% endblock %}
+
+{% block heading %}method not implemented{% endblock %}
+
+{% block body %}
+<p>You just tried to do something that <strong>erli</strong> can't or isn't supposed to handle (yet).</p>
+{% endblock %}
9 templates/error_handlers/serverfault.dtl
@@ -0,0 +1,9 @@
+{% extends "base.dtl" %}
+
+{% block title %}internal error{% endblock %}
+
+{% block heading %}internal error{% endblock %}
+
+{% block body %}
+<p>Whoops! The server just ran across some code that it couldn't handle. Your request cannot be processed further. A team of highly trained monkeys is working hard to fix this.</p>
+{% endblock %}
9 templates/error_handlers/unavailable.dtl
@@ -0,0 +1,9 @@
+{% extends "base.dtl" %}
+
+{% block title %}service unavailable{% endblock %}
+
+{% block heading %}service unavailable{% endblock %}
+
+{% block body %}
+<p><strong>erli</strong> is currently not able to handle your request.</p>
+{% endblock %}
81 templates/index.dtl
@@ -0,0 +1,81 @@
+{% extends "base.dtl" %}
+
+{% block title %}a URL shortener{% endblock %}
+
+{% block extra_js %}
+ <script type="text/javascript" src="http://twitter.github.com/bootstrap/1.4.0/bootstrap-modal.js"></script>
+ <script type="text/javascript" src="http://twitter.github.com/bootstrap/1.4.0/bootstrap-alerts.js"></script>
+ <script type="text/javascript" src="/static/erli.js"></script>
+{% endblock %}
+
+{% block heading %}an Erlang URL shortener{% endblock %}
+
+{% block body %}
+ <!-- Success banner after successful creation of a shortened URL -->
+ <div class="alert-message block-message success fade in" data-alert="alert" style="display:none;" id="success_banner">
+ <a class="close" href="#">X</a>
+ <p><strong>erli</strong> shortened your URL: <a href="" id="path"></a></p>
+ <p>You can view some statistics regarding usage of your shortened URL here: <a href="" id="path_stats"></a></p>
+ </div>
+
+ <!-- Warning banner for visitors with js disabled -->
+ <div class="alert-message warning">
+ <p><strong>Javascript disabled!</strong> It seems that you have javascript disabled, without it most of <strong>erli</strong> won't work. Please enable it.</p>
+ </div>
+
+ <!-- Error banner for shortening banned URLs -->
+ <div class="alert-message error" style="display:none;">
+ <p><strong>URL banned</strong> The URL (<span id="banned_url">url</span>) you are trying to shorten has been reported numerous times and has been banned</p>
+ <a class="close" href="#">X</a>
+ </div>
+
+ <form id="shorten_url_form">
+ <fieldset>
+ <div class="clearfix input">
+ <input class="xlarge" id="url_input" name="url" size="30" type="text" placeholder="enter a URL to shorten" />
+ <span class="help-inline" style="display:none;">Please enter a <span rel="popover" id="url_structure_popup" data-content="To be correct erli is an URI shortener, hence you must specify a schema for the link, otherwise bad things can happen during the 302 redirect. Additionally, erli currently only supports ASCI URIs." data-original-title="What is a correct URL?">correct URL</span> to shorten</span>
+ </div>
+ <div class="clearfix input">
+ <label for="pref_path_input"><em>Optional:</em> enter the shortened URL of your choice <span rel="popover" id="path_help_popup" data-content="In this case path just refers to the erli suffix your URL is shortened to. Feel free to chose your own! For example, to shorten a URL to just er.li/1 enter 1 here. You will (immediately) see if the path you choose is available." data-original-title="What is a path?">(huh?)</span></label><br />
+ <input class="xlarge" id="pref_path_input" type="text" name="path" placeholder="enter a shortened path" size="30" />
+ <span class="help-inline" style="display:none">This path is already taken</span>
+ </div>
+ <div class="clearfix input">
+ <label>
+ <input type="checkbox" id="tou_checkbox" name="tou_check" value="comply" />
+ <span>My URL complies with the <a href="#" data-controls-modal="modal-tou" data-backdrop="false" data-keyboard="true">Terms of Use</a></span>
+ </label>
+ </div>
+ <div class="clearfix">
+ <button id="url_submit" type="submit" class="btn">Shorten!</button>
+ </div>
+ </fieldset>
+ </form>
+
+ <p><strong>erli</strong> shortens your URLs and provides easily accessible statistics regarding usage of your shortened URL.</p>
+ <p>Not sure if you should be visiting the shortened URL? Just append <code>?landing=true</code> and <strong>erli</strong> will display a landing page with information regarding the target URL with the option to either visit the target or be redirected to Google.</p>
+{% endblock %}
+
+{% block modal_content %}
+ <div id="modal-tou" class="modal" style="display:none;">
+ <div class="modal-header">
+ <a href="#" class="close">X</a>
+ <h3><strong>erli</strong> :: Terms of Use</h3>
+ </div>
+ <div class="modal-body">
+ <p>The terms of use are simple.</p>
+ <p>Don't use shortify to shorten URLs that link to the following:</p>
+ <ul class="unstyled">
+ <li>Illegal content: e.g. child pornography</li>
+ <li>Harmful content: e.g. phishing websites</li>
+ </ul>
+ <p>If you are unsure whether your link infringes on one of the above, just don't submit it ;)</p>
+ <p>If you stumble upon a link which does infringe the conditions above, you can report it by simply appending <code>/report</code> to the infringing shortened URL.</p>
+ <p><strong>Example:</strong> assume that the shortened URL <code>http://er.li/asdf1</code> links to a page that doesn't seem to comply with the above terms. You can report the infringement by navigating to <code>http://er.li/asdf1/report</code>. Shortened URLs that get reported multiple times are removed.</p>
+ </div>
+ <div class="modal-footer">
+ <a href="#" class="btn primary">I agree</a>
+ <a href="#" class="btn secondary">Okay</a>
+ </div>
+ </div>
+{% endblock %}
11 templates/landing.dtl
@@ -0,0 +1,11 @@
+{% extends "base.dtl" %}
+
+{% block title %}landing{% endblock %}
+
+{% block heading %}landing{% endblock %}
+
+{% block body %}
+<p>Hello there, internaut! You are about to visit <code>{{ target }}</code></p>
+
+<a href="{{ path }}" class="btn primary">Take me there!</a> <a href="/" class="btn">That's not what I came for!</a>
+{% endblock %}
11 templates/report.dtl
@@ -0,0 +1,11 @@
+{% extends "base.dtl" %}
+
+{% block title %}report{% endblock %}
+
+{% block heading %}reporting ToU infringement{% endblock %}
+
+{% block body %}
+<p>You reported the shortened URL: <code>/{{ path }}</code> which points to <code>{{ target }}</code></p>
+<p>Thanks for your help enforcing the <strong>erli</strong> Terms of Use!</p>
+<p>Our mechanical monkeys will determine if the target URL should be purged permanently.</p>
+{% endblock %}
33 templates/stats.dtl
@@ -0,0 +1,33 @@
+{% extends "base.dtl" %}
+
+{% block title %}statistics{% endblock %}
+
+{% block extra_js %}
+<script type="text/javascript" src="/static/worldmap.js"></script>
+<script type="text/javascript">
+$(document).ready(function(){
+ WorldMap({
+ {% if surl.countries %}
+ detail: {
+ {% for country in surl.countries %}
+ "{{country}}": "#980000",
+ {% endfor %}
+ },
+ {% endif %}
+ id: "themap"
+ });
+});
+</script>
+{% endblock %}
+
+{% block heading %}usage statistics{% endblock %}
+
+{% block body %}
+<p>The shortened URL: <code>/{{ path }}</code> has been viewed a total of {{ total_clicks }} times, taking {{ unique_clicks }} visitors to <code>{{ target }}</code></p>
+
+<p>The visitors came from:</p>
+
+<canvas id="themap" width="800" height="325">
+
+<p>PS: these stats are updated hourly</p>
+{% endblock %}
Please sign in to comment.
Something went wrong with that request. Please try again.