Link to Project page (you’ll find the bookmarklet…there, mhehehe)
UPDATE:
- – April 16th, 2015 ~ [0.4] BugFix: fixed double ampersand escaping, causing & to go back on &.
- – April 15th, 2015 ~ [0.3] BugFix, Refactoring: Inconsitency in Places and Collections data caused undefined attributes in js. Splitted download file function from data elaboration. Escaped characters in KML file.
TO UPDATE YOUR MOD: please drag again the bookmarklet from the project page.
Some days ago I went to Berlin to meet with one of my old LILiK friends, @ieghermaister. Since he is developing some part of the Here maps engine and one of its best feature is the ability to download maps completely offline, he suggested me to download it on my phone and save the Berlin map. Once in Berlin, he wanted to suggests me some places and we struggled a bit finding a way to import or share favorites between users, or export them. We managed it by manually passing lists of urls in a shared document. Not smart at all.
Apart from this things, I really enjoyed the application, reminding me of the good old Ovi Maps symbian application, which rescued me from practically everywhere. So I decided to have a look at the web app code and see what was under the sheets.
Good News: AngularJs
Opening the inspector revealed a nice surprise. The guys at Here used AngularJs to develop the web interface, which gave me a good space of maneuver . First thing to do was to understand how to inject my code inside the app.
A simple overloading of the methods did not actually work, because angular after being loaded parse the DOM, including <script> tags, and store them in cache.
What about injecting other <script> tags and bootstrap again angular? That seemed a nice way to proceed, but will angular like the new tags? Simple answer,no. Caching is still in the middle of our process and will interfere with new code loading.
Let’s try to inject it in another way. What about the good old iFrames(arsh…)?
It could be possible to remove the content from the DOM, insert the HTML code with here.com modified inside the iframe before angular bootstraps. That was a nice plan, except that iFrame .load() method implementation is a little unpredictable and will not fire at the expected time. No luck for us.
So what’s the solution? Here it is. Remove everything form the DOM, request here.com page with ajax, modify the received content with our features, load it inside the DOM and bootstrap angular on the entire document. Easy peasy.
To start all this sequence I’ve decided to use the naive web object known with name of bookmaklet. To whom of you who don’t remember what these things are, basically it’s a bookmark with javascript inside the href (avoiding XSS problems!).
Hitting the button will load the main script from another source, an httpS one.
Let’s Code
Here is the bookmarklet source code:
1 2 3 4 5 |
javascript: (function () { var jsCode = document.createElement('script'); jsCode.setAttribute('src', 'https://cdn.rawgit.com/edoardoo/there/master/there.js'); document.body.appendChild(jsCode); }()); |
The script I wanted to inject is on github, but since they’ll not let you use it as a resource, I hat to use this awesome project rawgit.com (give it a try, really).
Here is the injected code that will make the whole magic, with escaped strings removed because of web publishing, fully available with strings on github:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
/* tHere, topping here.com maps app with some neat features url: http://edoardoo.github.io/there/ author: Edoardo Odorico, edoardoo.com, edo[at]lenotta.com license: MIT */ function loadScript(url, callback) { // Adding the script tag to the head as suggested before var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url; // Then bind the event to the callback function. // There are several events for cross browser compatibility. script.onreadystatechange = callback; script.onload = callback; // Fire the loading head.appendChild(script); } function show_loading(){ //show a little loading animation to let the user know something is going on var style = "<style>.rotate{-webkit-transition-property:-webkit-transform;-webkit-transition-duration:1s;-moz-transition-property:-moz-transform;-moz-transition-duration:1s;-webkit-animation-name:rotate;-webkit-animation-duration:3s;-webkit-animation-iteration-count:infinite;-webkit-animation-timing-function:linear;-moz-animation-name:rotate;-moz-animation-duration:3s;-moz-animation-iteration-count:infinite;-moz-animation-timing-function:linear}@-webkit-keyframes rotate{from{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@-moz-keyframes rotate{from{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(360deg)}}</style>"; $("head").append(style); $("#service_switcher .herelogo").addClass('rotate'); } function hide_loading(){ $("#service_switcher .herelogo").removeClass('rotate'); } function clean_code(code){ //strip out all the code we don't need return code = code.replace(/<(\/)?html.*>/g, '') .replace(/<head([^.]|[.])*\/head>/g, '') .replace(/<(\/)?body.*>/g, '') .replace(/<!doctype.*>/g, '') .replace('"use strict";',''); } var setStuff = function (){ //set our mod and let it run show_loading(); $.ajax({ url: 'https://www.here.com', success: function(code){ //prepare the modules to inject var backupImage = ''; var tags = '<script>@</script>' var modules = ''; modules = modules+''; var templates = ''; templates = templates + ''; var style = '<style>.favorite_box .address{top:6.3rem;}</style>'; //composing the injection: inject = tags.split("@").join(modules) + tags.split("@").join(templates) + style; //clean the code we received from here.com, removing head and other redundant tags code = clean_code(code); //inject! $('body').html(code+inject); //let angular know we did change the DOM structure angular.bootstrap(document, ['hereApp']); //just to be sure animation will not stuck loading hide_loading(); } }); } loadScript("https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js", setStuff); |
The (long) strings I’ve removed are the modified modules, controller and templates which will be inserted at the end of the retrieved html.
If you are curious about what’s inside, the beautified and unescaped version is available on github repo.
Basically, it introduces two new features:
- show places custom name (when available) instead of regular name and enable a
– backup feature that will download a KML file with all your favs organised by collection (ohh yeah).
It would have been very nice to have an import feature, but as we all know, time is a bitch. Code is released under MIT license, so if you want to implement it just fork the project on github and make a pull request.
What’s really good about it? That it’s not actually a hack. It uses the same data the app is using, modifying it, and doing this it respects its behaviour. Simply, amazing.
Have fun modifying your angular apps!
Comments
No comments yet.