osm2vcf.user.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. /**
  2. * This is a Greasemonkey(-compatible) script. It must be run from a
  3. * userscript manager such as Greasemonkey or Tampermonkey.
  4. */
  5. // ==UserScript==
  6. // @name OSM2VCF
  7. // @description Download OSM data as a vCard.
  8. // @include https://www.openstreetmap.org/node/*
  9. // @include https://www.openstreetmap.org/relation/*
  10. // @include https://www.openstreetmap.org/way/*
  11. // @version 1.2.0
  12. // @updateURL https://github.com/fabacab/osm2vcf/raw/master/osm2vcf.user.js
  13. // @grant GM.xmlHttpRequest
  14. // ==/UserScript==
  15. // Initialize.
  16. CONFIG = {};
  17. CONFIG.api_anchor = document.querySelector('[href^="/api"]');
  18. CONFIG.api_url = CONFIG.api_anchor.getAttribute('href');
  19. CONFIG.download_button = createDownloadLink();
  20. CONFIG.vCard = {};
  21. CONFIG.vCard.version = '3.0';
  22. /**
  23. * Makes the "Download VCF" link in the OSM Web interface.
  24. *
  25. * @return HTMLElement
  26. */
  27. function createDownloadLink () {
  28. var dl_btn = document.createElement('a');
  29. dl_btn.addEventListener('click', main);
  30. dl_btn.innerText = 'Download VCF';
  31. dl_btn.setAttribute('href', '#')
  32. dl_btn.setAttribute('download', window.location.pathname.match(/\d*$/))
  33. return dl_btn;
  34. }
  35. /**
  36. * Initialize the UI by creating a download link.
  37. */
  38. function init () {
  39. CONFIG.api_anchor.insertAdjacentElement('afterend', CONFIG.download_button);
  40. CONFIG.api_anchor.insertAdjacentHTML('afterend', ' · ');
  41. }
  42. /**
  43. * Receives the XMLHttpRequest response from the OSM API server.
  44. *
  45. * The body of this response should be an XML document in OSM style.
  46. *
  47. * @param {object} response
  48. * @return {object}
  49. */
  50. function parseApiResponse (response) {
  51. var m = response.finalUrl.match(/(node|way|relation)\/(\d+)(?:\/full)?/);
  52. if (null === m) { throw 'Unrecognized API call.'; }
  53. var id = m[2]; // OSM object ID.
  54. var oe = m[1]; // OSM element type.
  55. var r = {}; // Return object.
  56. var d = response.responseXML.documentElement;
  57. var el = d.querySelector(oe + '[id="'+ id +'"]');
  58. // Find meaningful tags associated with the requested object.
  59. var keys = [
  60. 'addr:city',
  61. 'addr:housenumber',
  62. 'addr:postcode',
  63. 'addr:state',
  64. 'addr:street',
  65. 'description',
  66. 'email',
  67. 'name',
  68. 'phone',
  69. 'website',
  70. ];
  71. for (var i = 0; i < keys.length; i++) {
  72. if (el.querySelector('tag[k="' + keys[i] + '"]')) {
  73. r[keys[i]] = el
  74. .querySelector('tag[k="' + keys[i] + '"]')
  75. .getAttribute('v');
  76. }
  77. }
  78. // Collect all the Nodes so we can deduce location/position.
  79. r['x-osm-member-nodes'] = Array.from(d.querySelectorAll('node'));
  80. // If the requested object explicitly labels its geographic center
  81. // then we can forget about the other Nodes and just note that one.
  82. var explicit_center = el.querySelector(
  83. 'member[type="node"][role="admin_centre"], member[type="node"][role="label"]'
  84. );
  85. if (explicit_center) {
  86. r['x-osm-member-nodes'] = [d.querySelector(
  87. '[id="' + explicit_center.getAttribute('ref') + '"]'
  88. )];
  89. }
  90. return r;
  91. }
  92. /**
  93. * Ensures lat/lon keys are included in the OSM object.
  94. *
  95. * OSM Node elements are the only OSM element that can be directly
  96. * associated with geographic coordinates, but many meaningful OSM
  97. * objects are represented as Ways or Relations. These objects have
  98. * one or more member nodes, and this function ensures the passed
  99. * object has a generally sensible geographic coordinate attached.
  100. *
  101. * @param {object} osm Object with OSM-formatted keys.
  102. * @return {object} OSM-formatted object with guaranteed lat/lon.
  103. */
  104. function normalizeGeographicCenter (osm) {
  105. var points = osm['x-osm-member-nodes'].map(function (el) {
  106. return {
  107. 'lat': el.getAttribute('lat'),
  108. 'lon': el.getAttribute('lon')
  109. };
  110. });
  111. var min_lat = Math.min(...points.map(function (p) {
  112. return p.lat;
  113. }));
  114. var max_lat = Math.max(...points.map(function (p) {
  115. return p.lat;
  116. }));
  117. var min_lon = Math.min(...points.map(function (p) {
  118. return p.lon;
  119. }));
  120. var max_lon = Math.max(...points.map(function (p) {
  121. return p.lon;
  122. }));
  123. osm.lat = ((min_lat + max_lat) / 2).toFixed(7);
  124. osm.lon = ((min_lon + max_lon) / 2).toFixed(7);
  125. return osm;
  126. }
  127. /**
  128. * Convert OSM tags to VCF-formatted fields.
  129. *
  130. * @param {object} osm Object with OSM-formatted keys.
  131. * @return {object} Object with VCF-formatted keys.
  132. */
  133. function osm2vcf (osm) {
  134. var vcf = {};
  135. vcf['KIND'] = 'org';
  136. keys = Object.keys(osm);
  137. addr = keys.filter(function (element) {
  138. return element.startsWith('addr:');
  139. });
  140. if (addr.length) {
  141. vcf['ADR'] = [
  142. '',
  143. osm['addr:housenumber'] || '',
  144. osm['addr:street'] || '',
  145. osm['addr:city'] || '',
  146. osm['addr:state'] || '',
  147. osm['addr:postcode'] || '',
  148. osm['addr:country'] || ''
  149. ].join(';');
  150. }
  151. if (osm.lat) {
  152. vcf['GEO'] = osm.lat + ',' + osm.lon;
  153. }
  154. if (osm.name) {
  155. vcf['ORG'] = osm.name;
  156. }
  157. if (osm.description) {
  158. vcf['NOTE'] = osm.description.replace(/\n/g, '\\n');
  159. }
  160. if (osm.email) {
  161. vcf['EMAIL'] = osm.email;
  162. }
  163. if (osm.phone) {
  164. vcf['TEL'] = osm.phone;
  165. }
  166. if (osm.website) {
  167. vcf['URI'] = osm.website;
  168. }
  169. return vcf;
  170. }
  171. /**
  172. * Write OSM data in VCF format.
  173. *
  174. * @param {object} data
  175. * @return {string}
  176. */
  177. function vCardWriter (data) {
  178. vcf_string = "BEGIN:VCARD";
  179. vcf_string += "\r\nVERSION:" + CONFIG.vCard.version;
  180. vcf_string += "\r\nPRODID:OSM2VCF Userscript";
  181. vcf_string += "\r\nREV:" + new Date().toISOString();
  182. if (data.KIND) {
  183. vcf_string += "\r\nKIND:" + data.KIND;
  184. }
  185. if (data.GEO) {
  186. vcf_string += "\r\nGEO:" + data.GEO;
  187. }
  188. if (data.ADR) {
  189. vcf_string += "\r\nADR:" + data.ADR;
  190. }
  191. if (data.FN) {
  192. vcf_string += "\r\nFN:" + data.FN;
  193. }
  194. if (data.TEL) {
  195. vcf_string += "\r\nTEL:" + data.TEL;
  196. }
  197. if (data.URI) {
  198. vcf_string += "\r\nURI:" + data.URI;
  199. }
  200. if (data.ORG) {
  201. vcf_string += "\r\nORG:" + data.ORG;
  202. }
  203. if (data.EMAIL) {
  204. vcf_string += "\r\nEMAIL:" + data.EMAIL;
  205. }
  206. if (data.NOTE) {
  207. vcf_string += "\r\nNOTE:" + data.NOTE;
  208. }
  209. vcf_string += "\r\nEND:VCARD";
  210. return vcf_string;
  211. }
  212. /**
  213. * Main entry point for the script.
  214. *
  215. * @param {MouseEvent} e
  216. */
  217. function main (e) {
  218. e.preventDefault();
  219. e.stopImmediatePropagation();
  220. // Use the `/full` endpoint for an OSM Way or Relation.
  221. // OSM Nodes don't have this endpoint, will return HTTP 404.
  222. var url = window.location.protocol + '//' + window.location.host + CONFIG.api_url
  223. if (-1 === CONFIG.api_url.indexOf('/node/')) {
  224. url += '/full';
  225. }
  226. GM.xmlHttpRequest({
  227. 'method': 'GET',
  228. 'synchronous': true,
  229. 'url': url,
  230. 'onload': function (response) {
  231. var b = new Blob([
  232. vCardWriter(
  233. osm2vcf(
  234. normalizeGeographicCenter(
  235. parseApiResponse(response)
  236. )
  237. )
  238. )
  239. ], { 'type': 'text/vcard' });
  240. CONFIG.download_button.setAttribute('href', URL.createObjectURL(b));
  241. window.location = CONFIG.download_button.getAttribute('href');
  242. }
  243. });
  244. }
  245. init();