attr2_options.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /**
  2. * @ngdoc service
  3. * @name Attr2Options
  4. * @description
  5. * Converts tag attributes to options used by google api v3 objects, map, marker, polygon, circle, etc.
  6. */
  7. /*jshint -W030*/
  8. ngMap.service('Attr2Options', ['$parse', 'NavigatorGeolocation', 'GeoCoder', function($parse, NavigatorGeolocation, GeoCoder) {
  9. var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
  10. var MOZ_HACK_REGEXP = /^moz([A-Z])/;
  11. var orgAttributes = function(el) {
  12. (el.length > 0) && (el = el[0]);
  13. var orgAttributes = {};
  14. for (var i=0; i<el.attributes.length; i++) {
  15. var attr = el.attributes[i];
  16. orgAttributes[attr.name] = attr.value;
  17. }
  18. return orgAttributes;
  19. }
  20. var camelCase = function(name) {
  21. return name.
  22. replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
  23. return offset ? letter.toUpperCase() : letter;
  24. }).
  25. replace(MOZ_HACK_REGEXP, 'Moz$1');
  26. }
  27. var JSONize = function(str) {
  28. try { // if parsable already, return as it is
  29. JSON.parse(str);
  30. return str;
  31. } catch(e) { // if not parsable, change little
  32. return str
  33. // wrap keys without quote with valid double quote
  34. .replace(/([\$\w]+)\s*:/g, function(_, $1){return '"'+$1+'":'})
  35. // replacing single quote wrapped ones to double quote
  36. .replace(/'([^']+)'/g, function(_, $1){return '"'+$1+'"'})
  37. }
  38. }
  39. var toOptionValue = function(input, options) {
  40. var output, key=options.key, scope=options.scope;
  41. try { // 1. Number?
  42. var num = Number(input);
  43. if (isNaN(num)) {
  44. throw "Not a number";
  45. } else {
  46. output = num;
  47. }
  48. } catch(err) {
  49. try { // 2.JSON?
  50. if (input.match(/^[\+\-]?[0-9\.]+,[ ]*\ ?[\+\-]?[0-9\.]+$/)) { // i.e "-1.0, 89.89"
  51. input = "["+input+"]";
  52. }
  53. output = JSON.parse(JSONize(input));
  54. if (output instanceof Array) {
  55. var t1stEl = output[0];
  56. if (t1stEl.constructor == Object) { // [{a:1}] : not lat/lng ones
  57. } else if (t1stEl.constructor == Array) { // [[1,2],[3,4]]
  58. output = output.map(function(el) {
  59. return new google.maps.LatLng(el[0], el[1]);
  60. });
  61. } else if(!isNaN(parseFloat(t1stEl)) && isFinite(t1stEl)) {
  62. return new google.maps.LatLng(output[0], output[1]);
  63. }
  64. }
  65. } catch(err2) {
  66. // 3. Object Expression. i.e. LatLng(80,-49)
  67. if (input.match(/^[A-Z][a-zA-Z0-9]+\(.*\)$/)) {
  68. try {
  69. var exp = "new google.maps."+input;
  70. output = eval(exp); // TODO, still eval
  71. } catch(e) {
  72. output = input;
  73. }
  74. // 4. Object Expression. i.e. MayTypeId.HYBRID
  75. } else if (input.match(/^([A-Z][a-zA-Z0-9]+)\.([A-Z]+)$/)) {
  76. try {
  77. var matches = input.match(/^([A-Z][a-zA-Z0-9]+)\.([A-Z]+)$/);
  78. output = google.maps[matches[1]][matches[2]];
  79. } catch(e) {
  80. output = input;
  81. }
  82. // 5. Object Expression. i.e. HYBRID
  83. } else if (input.match(/^[A-Z]+$/)) {
  84. try {
  85. var capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
  86. if (key.match(/temperatureUnit|windSpeedUnit|labelColor/)) {
  87. capitalizedKey = capitalizedKey.replace(/s$/,"");
  88. output = google.maps.weather[capitalizedKey][input];
  89. } else {
  90. output = google.maps[capitalizedKey][input];
  91. }
  92. } catch(e) {
  93. output = input;
  94. }
  95. } else {
  96. output = input;
  97. }
  98. } // catch(err2)
  99. } // catch(err)
  100. return output;
  101. };
  102. var setDelayedGeoLocation = function(object, method, param, options) {
  103. options = options || {};
  104. var centered = object.centered || options.centered;
  105. var errorFunc = function() {
  106. console.log('error occurred while', object, method, param, options);
  107. var fallbackLocation = options.fallbackLocation || new google.maps.LatLng(0,0);
  108. object[method](fallbackLocation);
  109. };
  110. if (!param || param.match(/^current/i)) { // sensored position
  111. NavigatorGeolocation.getCurrentPosition().then(
  112. function(position) { // success
  113. var lat = position.coords.latitude;
  114. var lng = position.coords.longitude;
  115. var latLng = new google.maps.LatLng(lat,lng);
  116. object[method](latLng);
  117. if (centered) {
  118. object.map.setCenter(latLng);
  119. }
  120. options.callback && options.callback.apply(object);
  121. },
  122. errorFunc
  123. );
  124. } else { //assuming it is address
  125. GeoCoder.geocode({address: param}).then(
  126. function(results) { // success
  127. object[method](results[0].geometry.location);
  128. if (centered) {
  129. object.map.setCenter(results[0].geometry.location);
  130. }
  131. },
  132. errorFunc
  133. );
  134. }
  135. };
  136. var getAttrsToObserve = function(attrs) {
  137. var attrsToObserve = [];
  138. if (attrs["ng-repeat"] || attrs.ngRepeat) { // if element is created by ng-repeat, don't observe any
  139. } else {
  140. for (var attrName in attrs) {
  141. var attrValue = attrs[attrName];
  142. if (attrValue && attrValue.match(/\{\{.*\}\}/)) { // if attr value is {{..}}
  143. console.log('setting attribute to observe', attrName, camelCase(attrName), attrValue);
  144. attrsToObserve.push(camelCase(attrName));
  145. }
  146. }
  147. }
  148. return attrsToObserve;
  149. };
  150. var observeAttrSetObj = function(orgAttrs, attrs, obj) {
  151. var attrsToObserve = getAttrsToObserve(orgAttrs);
  152. if (Object.keys(attrsToObserve).length) {
  153. console.log(obj, "attributes to observe", attrsToObserve);
  154. }
  155. for (var i=0; i<attrsToObserve.length; i++) {
  156. observeAndSet(attrs, attrsToObserve[i], obj);
  157. }
  158. }
  159. var observeAndSet = function(attrs, attrName, object) {
  160. attrs.$observe(attrName, function(val) {
  161. if (val) {
  162. console.log('observing ', object, attrName, val);
  163. var setMethod = camelCase('set-'+attrName);
  164. var optionValue = toOptionValue(val, {key: attrName});
  165. console.log('setting ', object, attrName, 'with value', optionValue);
  166. if (object[setMethod]) { //if set method does exist
  167. /* if an location is being observed */
  168. if (attrName.match(/center|position/) &&
  169. typeof optionValue == 'string') {
  170. setDelayedGeoLocation(object, setMethod, optionValue);
  171. } else {
  172. object[setMethod](optionValue);
  173. }
  174. }
  175. }
  176. });
  177. };
  178. return {
  179. /**
  180. * filters attributes by skipping angularjs methods $.. $$..
  181. * @memberof Attr2Options
  182. * @param {Hash} attrs tag attributes
  183. * @returns {Hash} filterd attributes
  184. */
  185. filter: function(attrs) {
  186. var options = {};
  187. for(var key in attrs) {
  188. if (key.match(/^\$/) || key.match(/^ng[A-Z]/)) {
  189. } else {
  190. options[key] = attrs[key];
  191. }
  192. }
  193. return options;
  194. },
  195. /**
  196. * converts attributes hash to Google Maps API v3 options
  197. * ```
  198. * . converts numbers to number
  199. * . converts class-like string to google maps instance
  200. * i.e. `LatLng(1,1)` to `new google.maps.LatLng(1,1)`
  201. * . converts constant-like string to google maps constant
  202. * i.e. `MapTypeId.HYBRID` to `google.maps.MapTypeId.HYBRID`
  203. * i.e. `HYBRID"` to `google.maps.MapTypeId.HYBRID`
  204. * ```
  205. * @memberof Attr2Options
  206. * @param {Hash} attrs tag attributes
  207. * @param {scope} scope angularjs scope
  208. * @returns {Hash} options converted attributess
  209. */
  210. getOptions: function(attrs, scope) {
  211. var options = {};
  212. for(var key in attrs) {
  213. if (attrs[key]) {
  214. if (key.match(/^on[A-Z]/)) { //skip events, i.e. on-click
  215. continue;
  216. } else if (key.match(/ControlOptions$/)) { // skip controlOptions
  217. continue;
  218. } else {
  219. options[key] = toOptionValue(attrs[key], {scope:scope, key: key});
  220. }
  221. } // if (attrs[key])
  222. } // for(var key in attrs)
  223. return options;
  224. },
  225. /**
  226. * converts attributes hash to scope-specific event function
  227. * @memberof Attr2Options
  228. * @param {scope} scope angularjs scope
  229. * @param {Hash} attrs tag attributes
  230. * @returns {Hash} events converted events
  231. */
  232. getEvents: function(scope, attrs) {
  233. var events = {};
  234. var toLowercaseFunc = function($1){
  235. return "_"+$1.toLowerCase();
  236. };
  237. var eventFunc = function(attrValue) {
  238. var matches = attrValue.match(/([^\(]+)\(([^\)]*)\)/);
  239. var funcName = matches[1];
  240. var argsStr = matches[2].replace(/event[ ,]*/,''); //remove string 'event'
  241. var args = scope.$eval("["+argsStr+"]");
  242. return function(event) {
  243. function index(obj,i) {return obj[i]}
  244. f = funcName.split('.').reduce(index, scope)
  245. f.apply(this, [event].concat(args));
  246. scope.$apply();
  247. }
  248. }
  249. for(var key in attrs) {
  250. if (attrs[key]) {
  251. if (!key.match(/^on[A-Z]/)) { //skip if not events
  252. continue;
  253. }
  254. //get event name as underscored. i.e. zoom_changed
  255. var eventName = key.replace(/^on/,'');
  256. eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
  257. eventName = eventName.replace(/([A-Z])/g, toLowercaseFunc);
  258. var attrValue = attrs[key];
  259. events[eventName] = new eventFunc(attrValue);
  260. }
  261. }
  262. return events;
  263. },
  264. /**
  265. * control means map controls, i.e streetview, pan, etc, not a general control
  266. * @memberof Attr2Options
  267. * @param {Hash} filtered filtered tag attributes
  268. * @returns {Hash} Google Map options
  269. */
  270. getControlOptions: function(filtered) {
  271. var controlOptions = {};
  272. if (typeof filtered != 'object')
  273. return false;
  274. for (var attr in filtered) {
  275. if (filtered[attr]) {
  276. if (!attr.match(/(.*)ControlOptions$/)) {
  277. continue; // if not controlOptions, skip it
  278. }
  279. //change invalid json to valid one, i.e. {foo:1} to {"foo": 1}
  280. var orgValue = filtered[attr];
  281. var newValue = orgValue.replace(/'/g, '"');
  282. newValue = newValue.replace(/([^"]+)|("[^"]+")/g, function($0, $1, $2) {
  283. if ($1) {
  284. return $1.replace(/([a-zA-Z0-9]+?):/g, '"$1":');
  285. } else {
  286. return $2;
  287. }
  288. });
  289. try {
  290. var options = JSON.parse(newValue);
  291. for (var key in options) { //assign the right values
  292. if (options[key]) {
  293. var value = options[key];
  294. if (typeof value === 'string') {
  295. value = value.toUpperCase();
  296. } else if (key === "mapTypeIds") {
  297. value = value.map( function(str) {
  298. if (str.match(/^[A-Z]+$/)) { // if constant
  299. return google.maps.MapTypeId[str.toUpperCase()];
  300. } else { // else, custom map-type
  301. return str;
  302. }
  303. });
  304. }
  305. if (key === "style") {
  306. var str = attr.charAt(0).toUpperCase() + attr.slice(1);
  307. var objName = str.replace(/Options$/,'')+"Style";
  308. options[key] = google.maps[objName][value];
  309. } else if (key === "position") {
  310. options[key] = google.maps.ControlPosition[value];
  311. } else {
  312. options[key] = value;
  313. }
  314. }
  315. }
  316. controlOptions[attr] = options;
  317. } catch (e) {
  318. console.error('invald option for', attr, newValue, e, e.stack);
  319. }
  320. }
  321. } // for
  322. return controlOptions;
  323. }, // function
  324. toOptionValue: toOptionValue,
  325. camelCase: camelCase,
  326. setDelayedGeoLocation: setDelayedGeoLocation,
  327. getAttrsToObserve: getAttrsToObserve,
  328. observeAndSet: observeAndSet,
  329. observeAttrSetObj: observeAttrSetObj,
  330. orgAttributes: orgAttributes
  331. }; // return
  332. }]);