Chez Florentin, Faustine, Aimé, Julien et Marine.
", 420 | "text": "Chez Florentin, Faustine, Aimé, Julien et Marine.", 421 | }, 422 | Object { 423 | "action": "commented", 424 | "date": "2018-04-22 17:57:56 UTC", 425 | "html": "c'est quoi ? une note personnelle ou le nom d'un établissement ?
", 426 | "text": "c'est quoi ? une note personnelle ou le nom d'un établissement ?", 427 | }, 428 | ], 429 | "date_created": "2018-04-22 17:54:49 UTC", 430 | "id": "1369237", 431 | "lat": "44.4068500", 432 | "lon": "0.5891000", 433 | "status": "open", 434 | "url": "https://api.openstreetmap.org/api/0.6/notes/1369237", 435 | }, 436 | Object { 437 | "comments": Array [ 438 | Object { 439 | "action": "opened", 440 | "date": "2018-01-16 15:28:34 UTC", 441 | "html": "carrefour market
", 442 | "text": "carrefour market", 443 | }, 444 | Object { 445 | "action": "closed", 446 | "date": "2018-04-20 11:43:00 UTC", 447 | "html": "", 448 | "text": "", 449 | "uid": "2533303", 450 | "user": "opline", 451 | "user_url": "https://api.openstreetmap.org/user/opline", 452 | }, 453 | ], 454 | "date_closed": "2018-04-20 11:43:00 UTC", 455 | "date_created": "2018-01-16 15:28:34 UTC", 456 | "id": "1270165", 457 | "lat": "44.4890700", 458 | "lon": "0.1802000", 459 | "reopen_url": "https://api.openstreetmap.org/api/0.6/notes/1270165/reopen", 460 | "status": "closed", 461 | "url": "https://api.openstreetmap.org/api/0.6/notes/1270165", 462 | }, 463 | Object { 464 | "close_url": "https://api.openstreetmap.org/api/0.6/notes/1106518/close", 465 | "comment_url": "https://api.openstreetmap.org/api/0.6/notes/1106518/comment", 466 | "comments": Array [ 467 | Object { 468 | "action": "opened", 469 | "date": "2017-08-16 15:41:56 UTC", 470 | "html": "Monplaisir
", 471 | "text": "Monplaisir", 472 | }, 473 | Object { 474 | "action": "commented", 475 | "date": "2017-08-16 15:42:43 UTC", 476 | "html": "Les Carbonnières 477 |
", 478 | "text": "Les Carbonnières 479 | ", 480 | }, 481 | Object { 482 | "action": "commented", 483 | "date": "2017-08-17 16:57:49 UTC", 484 | "html": "lieu-dit, rue ou résidence?
", 485 | "text": "lieu-dit, rue ou résidence?", 486 | "uid": "379182", 487 | "user": "Metaltyty", 488 | "user_url": "https://api.openstreetmap.org/user/Metaltyty", 489 | }, 490 | Object { 491 | "action": "commented", 492 | "date": "2017-08-24 15:44:07 UTC", 493 | "html": "Lieu dit
", 494 | "text": "Lieu dit", 495 | }, 496 | Object { 497 | "action": "commented", 498 | "date": "2017-10-22 16:06:12 UTC", 499 | "html": "Monplaisir ou les carbonnières ? Vous nous proposez deux noms
", 500 | "text": "Monplaisir ou les carbonnières ? Vous nous proposez deux noms", 501 | "uid": "2533303", 502 | "user": "opline", 503 | "user_url": "https://api.openstreetmap.org/user/opline", 504 | }, 505 | Object { 506 | "action": "commented", 507 | "date": "2018-04-08 19:35:56 UTC", 508 | "html": "on cloture, pas de réactions ?
", 509 | "text": "on cloture, pas de réactions ?", 510 | "uid": "2568974", 511 | "user": "Vinber-Num&Lib", 512 | "user_url": "https://api.openstreetmap.org/user/Vinber-Num&Lib", 513 | }, 514 | ], 515 | "date_created": "2017-08-16 15:41:56 UTC", 516 | "id": "1106518", 517 | "lat": "44.5063613", 518 | "lon": "0.2216578", 519 | "status": "open", 520 | "url": "https://api.openstreetmap.org/api/0.6/notes/1106518", 521 | }, 522 | ] 523 | `; 524 | 525 | exports[`XML helpers convertUserXmlToJson Should convert an User XML string into a proper JSON object 1`] = ` 526 | Object { 527 | "account_created": "2010-01-02T13:40:42Z", 528 | "blocks": Object { 529 | "received": Array [ 530 | Object { 531 | "active": "0", 532 | "count": "1", 533 | }, 534 | ], 535 | }, 536 | "changesets": Object { 537 | "count": "3616", 538 | }, 539 | "contributor-terms": Object { 540 | "agreed": "true", 541 | }, 542 | "description": " 543 | OpenStreetMap passionate, I contribute mainly in the west of France on various subjects : indoor mapping, street equipment, advertisement... 544 | ", 545 | "display_name": "PanierAvide", 546 | "id": "214436", 547 | "img": Object { 548 | "href": "https://www.openstreetmap.org/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBamNZIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--576881ff3e6c38ff381c9ecceff09761c61951db/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lOTVRBd2VERXdNRDRHT2daRlZBPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==--efdc66963555b7419037018ba117eb1fea762dc5/Tux_avatar.png", 549 | }, 550 | "roles": " ", 551 | "traces": Object { 552 | "count": "95", 553 | }, 554 | } 555 | `; 556 | 557 | exports[`XML helpers jsonToXml Should convert a JSON object into an XML string 1`] = ` 558 | " 559 |Chez Florentin, Faustine, Aimé, Julien et Marine.
", 598 | ], 599 | "text": Array [ 600 | "Chez Florentin, Faustine, Aimé, Julien et Marine.", 601 | ], 602 | }, 603 | Object { 604 | "action": Array [ 605 | "commented", 606 | ], 607 | "date": Array [ 608 | "2018-04-22 17:57:56 UTC", 609 | ], 610 | "html": Array [ 611 | "c'est quoi ? une note personnelle ou le nom d'un établissement ?
", 612 | ], 613 | "text": Array [ 614 | "c'est quoi ? une note personnelle ou le nom d'un établissement ?", 615 | ], 616 | }, 617 | ], 618 | }, 619 | ], 620 | "date_created": Array [ 621 | "2018-04-22 17:54:49 UTC", 622 | ], 623 | "id": Array [ 624 | "1369237", 625 | ], 626 | "status": Array [ 627 | "open", 628 | ], 629 | "url": Array [ 630 | "https://api.openstreetmap.org/api/0.6/notes/1369237", 631 | ], 632 | }, 633 | Object { 634 | "$": Object { 635 | "lat": "44.4890700", 636 | "lon": "0.1802000", 637 | }, 638 | "comments": Array [ 639 | Object { 640 | "comment": Array [ 641 | Object { 642 | "action": Array [ 643 | "opened", 644 | ], 645 | "date": Array [ 646 | "2018-01-16 15:28:34 UTC", 647 | ], 648 | "html": Array [ 649 | "carrefour market
", 650 | ], 651 | "text": Array [ 652 | "carrefour market", 653 | ], 654 | }, 655 | Object { 656 | "action": Array [ 657 | "closed", 658 | ], 659 | "date": Array [ 660 | "2018-04-20 11:43:00 UTC", 661 | ], 662 | "html": Array [ 663 | "", 664 | ], 665 | "text": Array [ 666 | "", 667 | ], 668 | "uid": Array [ 669 | "2533303", 670 | ], 671 | "user": Array [ 672 | "opline", 673 | ], 674 | "user_url": Array [ 675 | "https://api.openstreetmap.org/user/opline", 676 | ], 677 | }, 678 | ], 679 | }, 680 | ], 681 | "date_closed": Array [ 682 | "2018-04-20 11:43:00 UTC", 683 | ], 684 | "date_created": Array [ 685 | "2018-01-16 15:28:34 UTC", 686 | ], 687 | "id": Array [ 688 | "1270165", 689 | ], 690 | "reopen_url": Array [ 691 | "https://api.openstreetmap.org/api/0.6/notes/1270165/reopen", 692 | ], 693 | "status": Array [ 694 | "closed", 695 | ], 696 | "url": Array [ 697 | "https://api.openstreetmap.org/api/0.6/notes/1270165", 698 | ], 699 | }, 700 | Object { 701 | "$": Object { 702 | "lat": "44.5063613", 703 | "lon": "0.2216578", 704 | }, 705 | "close_url": Array [ 706 | "https://api.openstreetmap.org/api/0.6/notes/1106518/close", 707 | ], 708 | "comment_url": Array [ 709 | "https://api.openstreetmap.org/api/0.6/notes/1106518/comment", 710 | ], 711 | "comments": Array [ 712 | Object { 713 | "comment": Array [ 714 | Object { 715 | "action": Array [ 716 | "opened", 717 | ], 718 | "date": Array [ 719 | "2017-08-16 15:41:56 UTC", 720 | ], 721 | "html": Array [ 722 | "Monplaisir
", 723 | ], 724 | "text": Array [ 725 | "Monplaisir", 726 | ], 727 | }, 728 | Object { 729 | "action": Array [ 730 | "commented", 731 | ], 732 | "date": Array [ 733 | "2017-08-16 15:42:43 UTC", 734 | ], 735 | "html": Array [ 736 | "Les Carbonnières 737 |
", 738 | ], 739 | "text": Array [ 740 | "Les Carbonnières 741 | ", 742 | ], 743 | }, 744 | Object { 745 | "action": Array [ 746 | "commented", 747 | ], 748 | "date": Array [ 749 | "2017-08-17 16:57:49 UTC", 750 | ], 751 | "html": Array [ 752 | "lieu-dit, rue ou résidence?
", 753 | ], 754 | "text": Array [ 755 | "lieu-dit, rue ou résidence?", 756 | ], 757 | "uid": Array [ 758 | "379182", 759 | ], 760 | "user": Array [ 761 | "Metaltyty", 762 | ], 763 | "user_url": Array [ 764 | "https://api.openstreetmap.org/user/Metaltyty", 765 | ], 766 | }, 767 | Object { 768 | "action": Array [ 769 | "commented", 770 | ], 771 | "date": Array [ 772 | "2017-08-24 15:44:07 UTC", 773 | ], 774 | "html": Array [ 775 | "Lieu dit
", 776 | ], 777 | "text": Array [ 778 | "Lieu dit", 779 | ], 780 | }, 781 | Object { 782 | "action": Array [ 783 | "commented", 784 | ], 785 | "date": Array [ 786 | "2017-10-22 16:06:12 UTC", 787 | ], 788 | "html": Array [ 789 | "Monplaisir ou les carbonnières ? Vous nous proposez deux noms
", 790 | ], 791 | "text": Array [ 792 | "Monplaisir ou les carbonnières ? Vous nous proposez deux noms", 793 | ], 794 | "uid": Array [ 795 | "2533303", 796 | ], 797 | "user": Array [ 798 | "opline", 799 | ], 800 | "user_url": Array [ 801 | "https://api.openstreetmap.org/user/opline", 802 | ], 803 | }, 804 | Object { 805 | "action": Array [ 806 | "commented", 807 | ], 808 | "date": Array [ 809 | "2018-04-08 19:35:56 UTC", 810 | ], 811 | "html": Array [ 812 | "on cloture, pas de réactions ?
", 813 | ], 814 | "text": Array [ 815 | "on cloture, pas de réactions ?", 816 | ], 817 | "uid": Array [ 818 | "2568974", 819 | ], 820 | "user": Array [ 821 | "Vinber-Num&Lib", 822 | ], 823 | "user_url": Array [ 824 | "https://api.openstreetmap.org/user/Vinber-Num&Lib", 825 | ], 826 | }, 827 | ], 828 | }, 829 | ], 830 | "date_created": Array [ 831 | "2017-08-16 15:41:56 UTC", 832 | ], 833 | "id": Array [ 834 | "1106518", 835 | ], 836 | "status": Array [ 837 | "open", 838 | ], 839 | "url": Array [ 840 | "https://api.openstreetmap.org/api/0.6/notes/1106518", 841 | ], 842 | }, 843 | ], 844 | }, 845 | } 846 | `; 847 | -------------------------------------------------------------------------------- /src/requests.js: -------------------------------------------------------------------------------- 1 | import { fetch, authxhr } from 'helpers/xhr'; 2 | import { 3 | findElementType, 4 | findElementId, 5 | checkIdIsNegative, 6 | simpleObjectDeepClone, 7 | buildApiUrl 8 | } from 'helpers/utils'; 9 | import { 10 | buildChangesetXml, 11 | buildChangesetFromObjectXml, 12 | convertNotesXmlToJson, 13 | convertElementXmlToJson, 14 | convertElementsListXmlToJson, 15 | convertUserXmlToJson, 16 | jsonToXml, 17 | xmlToJson, 18 | cleanMapJson, 19 | buildPreferencesFromObjectXml 20 | } from 'helpers/xml'; 21 | import { RequestException } from 'exceptions/request'; 22 | 23 | /** 24 | * Request to fetch an OSM element 25 | * @param {string} apiUrl The API URL 26 | * @param {string} osmId 27 | * @param {Object} [options] Options 28 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 29 | * @return {Object} 30 | */ 31 | export function fetchElementRequest(apiUrl, osmId, options = {}) { 32 | const elementType = findElementType(osmId); 33 | const elementId = findElementId(osmId); 34 | 35 | return fetch(buildApiUrl(apiUrl, `/${osmId}`), options).then(response => 36 | convertElementXmlToJson(response, elementType, elementId) 37 | ); 38 | } 39 | 40 | /** 41 | * Request to fetch way or relation and all other elements referenced by it 42 | * @param {string} apiUrl The API URL 43 | * @param {string} osmId Can only contain either a way or a relation 44 | * @param {Object} [options] Options 45 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 46 | * @return {Promise} Promise with well formatted JSON content 47 | */ 48 | export function fetchElementRequestFull(apiUrl, osmId, options = {}) { 49 | return fetch(buildApiUrl(apiUrl, `/${osmId}/full`), options).then(response => 50 | xmlToJson(response) 51 | .then(json => Promise.resolve(cleanMapJson(json))) 52 | .catch(error => { 53 | throw new RequestException(error); 54 | }) 55 | ); 56 | } 57 | 58 | /** 59 | * Request to fetch an OSM element 60 | * @param {string} apiUrl The API URL 61 | * @param {Array} osmIds Eg: ['node/12345', 'node/6789']. We do not support optional version e.g 'node/12345v2' 62 | * @param {Object} [options] Options 63 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 64 | * @return {Promise} 65 | */ 66 | export function multiFetchElementsByTypeRequest(apiUrl, osmIds, options = {}) { 67 | const elementType = findElementType(osmIds[0]); 68 | const ids = osmIds.map(osmId => findElementId(osmId)); 69 | return fetch( 70 | buildApiUrl(apiUrl, `/${elementType}s?${elementType}s=${ids.join(',')}`), 71 | options 72 | ).then(response => 73 | xmlToJson(response) 74 | .then(json => Promise.resolve(cleanMapJson(json))) 75 | .catch(error => { 76 | throw new RequestException(error); 77 | }) 78 | ); 79 | } 80 | 81 | /** 82 | * Request to fetch ways using the given OSM node 83 | * @param {string} apiUrl The API URL 84 | * @param {string} osmId 85 | * @param {Object} [options] Options 86 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 87 | * @return {Object} 88 | */ 89 | export function fetchWaysForNodeRequest(apiUrl, osmId, options = {}) { 90 | return fetch(buildApiUrl(apiUrl, `/${osmId}/ways`), options).then(response => 91 | convertElementsListXmlToJson(response, 'way') 92 | ); 93 | } 94 | 95 | /** 96 | * Send an element to OSM 97 | * @param {osmAuth} auth An instance of osm-auth 98 | * @param {string} apiUrl The API URL 99 | * @param {Object} element 100 | * @param {number} changesetId 101 | * @return {Promise} 102 | */ 103 | export function sendElementRequest(auth, apiUrl, element, changesetId) { 104 | const copiedElement = simpleObjectDeepClone(element); 105 | const { _id: elementId, _type: elementType } = copiedElement; 106 | delete copiedElement._id; 107 | delete copiedElement._type; 108 | 109 | copiedElement.$.changeset = changesetId; 110 | 111 | const osmContent = { 112 | osm: { 113 | $: {} 114 | } 115 | }; 116 | osmContent.osm[elementType] = [copiedElement]; 117 | 118 | const elementXml = jsonToXml(osmContent); 119 | const path = 120 | elementId && !checkIdIsNegative(elementId) 121 | ? `/${elementType}/${elementId}` 122 | : `/${elementType}/create`; 123 | 124 | return authxhr( 125 | { 126 | method: 'PUT', 127 | prefix: false, 128 | path: buildApiUrl(apiUrl, path), 129 | options: { 130 | header: { 131 | 'Content-Type': 'text/xml' 132 | } 133 | }, 134 | content: elementXml 135 | }, 136 | auth 137 | ).then(version => parseInt(version, 10)); 138 | } 139 | 140 | /** 141 | * Request to fetch OSM notes 142 | * @param {string} apiUrl The API URL 143 | * @param {number} left The minimal longitude (X) 144 | * @param {number} bottom The minimal latitude (Y) 145 | * @param {number} right The maximal longitude (X) 146 | * @param {number} top The maximal latitude (Y) 147 | * @param {number} [limit] The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) 148 | * @param {number} [closedDays] The amount of days a note needs to be closed to no longer be returned (defaults to 7, 0 means only opened notes are returned, and -1 means all notes are returned) 149 | * @param {Object} [options] Options 150 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 151 | * @return {Object} 152 | */ 153 | export function fetchNotesRequest( 154 | apiUrl, 155 | left, 156 | bottom, 157 | right, 158 | top, 159 | limit = null, 160 | closedDays = null, 161 | options = {} 162 | ) { 163 | const params = { 164 | bbox: `${left.toString()},${bottom.toString()},${right.toString()},${top.toString()}` 165 | }; 166 | 167 | if (limit) { 168 | params.limit = limit; 169 | } 170 | 171 | if (closedDays !== null && typeof closedDays !== 'undefined') { 172 | params.closed = closedDays; 173 | } 174 | 175 | return fetch(buildApiUrl(apiUrl, '/notes', params), options).then(response => 176 | convertNotesXmlToJson(response) 177 | ); 178 | } 179 | 180 | /** 181 | * Request to get OSM notes with textual search 182 | * @param {string} apiUrl The API URL 183 | * @param {string} q Specifies the search query 184 | * @param {string} [format] It can be 'xml' (default) to get OSM 185 | * and convert to JSON, 'raw' to return raw OSM XML, 'json' to 186 | * return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS 187 | * @param {number} [limit] The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) 188 | * @param {number} [closed] The amount of days a note needs to be closed to no longer be returned (defaults to 7, 0 means only opened notes are returned, and -1 means all notes are returned) 189 | * @param {string} [display_name] Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter 190 | * @param {number} [user] Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display_name parameter 191 | * @param {number} [from] Specifies the beginning of a date range to search in for a note 192 | * @param {number} [to] Specifies the end of a date range to search in for a note. Today date is the default 193 | * @param {Object} [options] Options 194 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 195 | * @return {Promise} 196 | */ 197 | export function fetchNotesSearchRequest( 198 | apiUrl, 199 | q, 200 | format = 'xml', 201 | limit = null, 202 | closed = null, 203 | display_name = null, 204 | user = null, 205 | from = null, 206 | to = null, 207 | options = {} 208 | ) { 209 | const params = { 210 | q 211 | }; 212 | 213 | let path = `/notes/search.${format}`; 214 | if (format === 'raw') { 215 | path = `/notes/search`; 216 | } 217 | 218 | const objectOptionalArgs = { 219 | limit, 220 | closed, 221 | display_name, 222 | user, 223 | from, 224 | to 225 | }; 226 | 227 | Object.entries(objectOptionalArgs).forEach(optional => { 228 | if (optional[1]) { 229 | params[optional[0]] = optional[1]; 230 | } 231 | }); 232 | 233 | return fetch(buildApiUrl(apiUrl, path, params), options).then(text => { 234 | if (format === 'xml') { 235 | return convertNotesXmlToJson(text); 236 | } else { 237 | return text; 238 | } 239 | }); 240 | } 241 | 242 | /** 243 | * Request to fetch OSM note by id 244 | * @param {string} apiUrl The API URL 245 | * param {number} noteId Identifier for the note 246 | * @param {string} format It can be 'xml' (default) to get OSM 247 | * and convert to JSON, 'raw' to return raw OSM XML, 'json' to 248 | * return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS 249 | * @param {Object} [options] Options 250 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 251 | * @return {Promise} 252 | */ 253 | export function fetchNoteByIdRequest( 254 | apiUrl, 255 | noteId, 256 | format = 'xml', 257 | options = {} 258 | ) { 259 | let path = `/notes/${noteId.toString()}.${format}`; 260 | if (format === 'raw') { 261 | path = `/notes/${noteId.toString()}`; 262 | } 263 | return fetch(buildApiUrl(apiUrl, path), options).then(text => { 264 | if (format === 'xml') { 265 | return convertNotesXmlToJson(text); 266 | } else { 267 | return text; 268 | } 269 | }); 270 | } 271 | 272 | /** 273 | * Request generic enough to manage all POST request for a particular note 274 | * @param {osmAuth} auth An instance of osm-auth 275 | * @param {string} apiUrl The API URL 276 | * param {number} noteId Identifier for the note 277 | * @param {string} text A mandatory text field with arbitrary text containing the note 278 | * @param {string} type Mandatory type. It can be 'comment', 'close' or 'reopen' 279 | * @return {Promise} 280 | */ 281 | export function genericPostNoteRequest(auth, apiUrl, noteId, text, type) { 282 | return authxhr( 283 | { 284 | method: 'POST', 285 | prefix: false, 286 | path: buildApiUrl(apiUrl, `/notes/${noteId}/${type}`, { text }), 287 | options: { 288 | header: { 289 | 'Content-Type': 'text/xml' 290 | } 291 | } 292 | }, 293 | auth 294 | ) 295 | .then(txt => convertNotesXmlToJson(txt)) 296 | .then(arr => arr.find(() => true)); 297 | } 298 | 299 | /** 300 | * Request to create a note 301 | * @param {osmAuth} auth An instance of osm-auth 302 | * @param {string} apiUrl The API URL 303 | * @param {number} lat Specifies the latitude of the note 304 | * @param {number} lon Specifies the longitude of the note 305 | * @param {string} text A mandatory text field with arbitrary text containing the note 306 | * @return {Promise} 307 | */ 308 | export function createNoteRequest(auth, apiUrl, lat, lon, text) { 309 | const params = { 310 | lat, 311 | lon, 312 | text 313 | }; 314 | return authxhr( 315 | { 316 | method: 'POST', 317 | prefix: false, 318 | path: buildApiUrl(apiUrl, '/notes', params), 319 | options: { 320 | header: { 321 | 'Content-Type': 'text/xml' 322 | } 323 | } 324 | }, 325 | auth 326 | ) 327 | .then(txt => convertNotesXmlToJson(txt)) 328 | .then(arr => arr.find(() => true)); 329 | } 330 | 331 | /** 332 | * Request to create OSM changeset 333 | * @param {osmAuth} auth An instance of osm-auth 334 | * @param {string} apiUrl The API URL 335 | * @param {string} [createdBy] 336 | * @param {string} [comment] 337 | * @param {string} [tags] An object with keys values to set to tags 338 | * @return {Promise} 339 | */ 340 | export function createChangesetRequest( 341 | auth, 342 | apiUrl, 343 | createdBy = '', 344 | comment = '', 345 | tags = {} 346 | ) { 347 | const changesetXml = buildChangesetXml(createdBy, comment, tags); 348 | 349 | return authxhr( 350 | { 351 | method: 'PUT', 352 | prefix: false, 353 | path: buildApiUrl(apiUrl, '/changeset/create'), 354 | options: { 355 | header: { 356 | 'Content-Type': 'text/xml' 357 | } 358 | }, 359 | content: changesetXml 360 | }, 361 | auth 362 | ).then(changesetId => parseInt(changesetId, 10)); 363 | } 364 | 365 | /** 366 | * Checks if a given changeset is still opened at OSM. 367 | * @param {string} apiUrl The API URL 368 | * @param {number} changesetId 369 | * @param {Object} [options] Options 370 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 371 | * @return {Promise} 372 | */ 373 | export function changesetCheckRequest(apiUrl, changesetId, options = {}) { 374 | return changesetGetRequest(apiUrl, changesetId, options).then(res => { 375 | let isOpened; 376 | 377 | try { 378 | isOpened = res.osm.changeset[0].$.open === 'true'; 379 | } catch (e) { 380 | isOpened = false; 381 | } 382 | 383 | if (!isOpened) { 384 | throw new Error('Changeset not opened'); 385 | } 386 | 387 | return changesetId; 388 | }); 389 | } 390 | 391 | /** 392 | * Get a changeset for a given id at OSM. 393 | * @param {string} apiUrl The API URL 394 | * @param {number} changesetId 395 | * @param {Object} [options] Options 396 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 397 | * @return {Promise} 398 | */ 399 | export function changesetGetRequest(apiUrl, changesetId, options = {}) { 400 | return fetch( 401 | buildApiUrl(apiUrl, `/changeset/${changesetId.toString()}`), 402 | options 403 | ).then(response => xmlToJson(response)); 404 | } 405 | /** 406 | * Update tags if a given changeset is still opened at OSM. 407 | * @param {osmAuth} auth An instance of osm-auth 408 | * @param {string} apiUrl The API URL 409 | * @param {number} changesetId 410 | * @param {string} [createdBy] 411 | * @param {string} [comment] 412 | * @param {Object} [tags] Use to set multiples tags 413 | * @throws Will throw an error for any request with http code 40x. 414 | * @return {Promise} 415 | */ 416 | export function updateChangesetTagsRequest( 417 | auth, 418 | apiUrl, 419 | changesetId, 420 | createdBy = '', 421 | comment = '', 422 | tags = {} 423 | ) { 424 | const changesetXml = buildChangesetFromObjectXml(tags, createdBy, comment); 425 | return authxhr( 426 | { 427 | method: 'PUT', 428 | prefix: false, 429 | path: buildApiUrl(apiUrl, `/changeset/${changesetId.toString()}`), 430 | options: { 431 | header: { 432 | 'Content-Type': 'text/xml' 433 | } 434 | }, 435 | content: changesetXml 436 | }, 437 | auth 438 | ).then(txt => xmlToJson(txt)); 439 | } 440 | 441 | /** 442 | * Request to close changeset for a given id if still opened 443 | * @param {osmAuth} auth An instance of osm-auth 444 | * @param {string} apiUrl The API URL 445 | * @param {number} changesetId 446 | * @throws Will throw an error for any request with http code 40x. 447 | * @return {Promise} Empty string if it works 448 | */ 449 | export function closeChangesetRequest(auth, apiUrl, changesetId) { 450 | return authxhr( 451 | { 452 | method: 'PUT', 453 | prefix: false, 454 | path: buildApiUrl(apiUrl, `/changeset/${changesetId.toString()}/close`), 455 | options: { 456 | header: { 457 | 'Content-Type': 'text/plain' 458 | } 459 | } 460 | }, 461 | auth 462 | ); 463 | } 464 | 465 | /** 466 | * Request to upload an OSC file content conforming to the OsmChange specification OSM changeset 467 | * @param {osmAuth} auth An instance of osm-auth 468 | * @param {string} apiUrl The API URL 469 | * @param {string} changesetId 470 | * @param {string} osmChangeContent OSC file content text 471 | * @return {Promise} 472 | */ 473 | export function uploadChangesetOscRequest( 474 | auth, 475 | apiUrl, 476 | changesetId, 477 | osmChangeContent 478 | ) { 479 | return authxhr( 480 | { 481 | method: 'POST', 482 | prefix: false, 483 | path: buildApiUrl(apiUrl, `/changeset/create`), 484 | options: { 485 | header: { 486 | 'Content-Type': 'text/xml' 487 | } 488 | }, 489 | content: osmChangeContent 490 | }, 491 | auth 492 | ).then(txt => xmlToJson(txt)); 493 | } 494 | 495 | /** 496 | * Request to get changesets from OSM API 497 | * @param {string} apiUrl The API URL 498 | * @param {Object} options Optional parameters 499 | * @param {number} [options.left] The minimal longitude (X) 500 | * @param {number} [options.bottom] The minimal latitude (Y) 501 | * @param {number} [options.right] The maximal longitude (X) 502 | * @param {number} [options.top] The maximal latitude (Y) 503 | * @param {string} [options.display_name] Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter 504 | * @param {number} [options.user] Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display_name parameter 505 | * @param {string} [options.time] Can be a unique value T1 or two values T1, T2 comma separated. Find changesets closed after value T1 or find changesets that were closed after T1 and created before T2. In other words, any changesets that were open at some time during the given time range T1 to T2. Time format is anything that http://ruby-doc.org/stdlib-2.6.3/libdoc/date/rdoc/DateTime.html#method-c-parse can parse. 506 | * @param {number} [options.open] Only finds changesets that are still open but excludes changesets that are closed or have reached the element limit for a changeset (50.000 at the moment). Can be set to true 507 | * @param {number} [options.closed] Only finds changesets that are closed or have reached the element limit. Can be set to true 508 | * @param {number} [options.changesets] Finds changesets with the specified ids 509 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 510 | * @return {Promise} 511 | */ 512 | export function fetchChangesetsRequest(apiUrl, options = {}) { 513 | const keys = [ 514 | 'left', 515 | 'bottom', 516 | 'right', 517 | 'top', 518 | 'display_name', 519 | 'user', 520 | 'time', 521 | 'open', 522 | 'closed', 523 | 'changesets' 524 | ]; 525 | 526 | const params = {}; 527 | keys.forEach(key => { 528 | if (key in options && options[key]) { 529 | params[key] = options[key].toString(); 530 | } 531 | }); 532 | 533 | if (params.left && params.bottom && params.right && params.top) { 534 | params.bbox = `${params.left.toString()},${params.bottom.toString()},${params.right.toString()},${params.top.toString()}`; 535 | delete params.left; 536 | delete params.bottom; 537 | delete params.right; 538 | delete params.top; 539 | } 540 | 541 | return fetch(buildApiUrl(apiUrl, '/changesets', params), { 542 | auth: options.auth 543 | }).then(text => xmlToJson(text)); 544 | } 545 | 546 | /** 547 | * Request to fetch all OSM elements within a bbox extent 548 | * @param {string} apiUrl The API URL 549 | * @param {number} left The minimal longitude (X) 550 | * @param {number} bottom The minimal latitude (Y) 551 | * @param {number} right The maximal longitude (X) 552 | * @param {number} top The maximal latitude (Y) 553 | * @param {string} mode The mode is json so output in the promise will be an object, otherwise, it will be an object and a XML string 554 | * @param {Object} [options] Options 555 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 556 | * @return {Promise} 557 | */ 558 | export function fetchMapByBboxRequest( 559 | apiUrl, 560 | left, 561 | bottom, 562 | right, 563 | top, 564 | mode = 'json', 565 | options = {} 566 | ) { 567 | const args = Array.from(arguments); 568 | if (args.length < 5 && args.some(arg => typeof arg === 'undefined')) { 569 | throw new Error("You didn't provide all arguments to the function"); 570 | } else { 571 | const params = { 572 | bbox: `${left.toString()},${bottom.toString()},${right.toString()},${top.toString()}` 573 | }; 574 | 575 | return fetch(buildApiUrl(apiUrl, '/map', params), options).then( 576 | response => { 577 | if (mode !== 'json') { 578 | return Promise.all([ 579 | xmlToJson(response) 580 | .then(json => Promise.resolve(cleanMapJson(json))) 581 | .catch(error => { 582 | throw new RequestException(error); 583 | }), 584 | response 585 | ]); 586 | } else { 587 | return xmlToJson(response) 588 | .then(json => Promise.resolve(cleanMapJson(json))) 589 | .catch(error => { 590 | throw new RequestException(error); 591 | }); 592 | } 593 | } 594 | ); 595 | } 596 | } 597 | 598 | /** 599 | * Delete an OSM element 600 | * @param {osmAuth} auth An instance of osm-auth 601 | * @param {string} apiUrl The API URL 602 | * @param {Object} element 603 | * @param {number} changesetId 604 | * @return {Promise} Promise with the new version number due to deletion 605 | */ 606 | export function deleteElementRequest(auth, apiUrl, element, changesetId) { 607 | const copiedElement = simpleObjectDeepClone(element); 608 | const { _id: elementId, _type: elementType } = copiedElement; 609 | delete copiedElement._id; 610 | delete copiedElement._type; 611 | 612 | copiedElement.$.changeset = changesetId; 613 | 614 | const osmContent = { 615 | osm: { 616 | $: {} 617 | } 618 | }; 619 | osmContent.osm[elementType] = [copiedElement]; 620 | 621 | const elementXml = jsonToXml(osmContent); 622 | const path = `/${elementType}/${elementId}`; 623 | 624 | return authxhr( 625 | { 626 | method: 'DELETE', 627 | prefix: false, 628 | path: buildApiUrl(apiUrl, path), 629 | options: { 630 | header: { 631 | 'Content-Type': 'text/xml' 632 | } 633 | }, 634 | content: elementXml 635 | }, 636 | auth 637 | ).then(version => parseInt(version, 10)); 638 | } 639 | 640 | /** Request to fetch relation(s) from an OSM element 641 | * @param {string} apiUrl The API URL 642 | * @param {string} osmId 643 | * @param {Object} [options] Options 644 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 645 | * @return {Promise} 646 | */ 647 | export function fetchRelationsForElementRequest(apiUrl, osmId, options = {}) { 648 | return fetch(buildApiUrl(apiUrl, `/${osmId}/relations`), options).then( 649 | response => convertElementsListXmlToJson(response, 'relation') 650 | ); 651 | } 652 | 653 | /** 654 | * Request to fetch an OSM user details 655 | * @param {string} apiUrl The API URL 656 | * @param {string} userId The user ID 657 | * @param {Object} [options] Options 658 | * @param {Object} [options.auth] Auth XHR object to use instead of unauthenticated call 659 | * @return {Object} 660 | */ 661 | export function fetchUserRequest(apiUrl, userId, options = {}) { 662 | return fetch(buildApiUrl(apiUrl, `/user/${userId}`), options).then(response => 663 | convertUserXmlToJson(response) 664 | ); 665 | } 666 | 667 | /** 668 | * Request to fetch preferences for the connected user 669 | * @param {osmAuth} auth An instance of osm-auth 670 | * @param {string} apiUrl The API URL 671 | * @throws Will throw an error for any request with http code 40x. 672 | * @return {Promise} Promise with the value for the key 673 | */ 674 | export function getUserPreferencesRequest(auth, apiUrl) { 675 | return authxhr( 676 | { 677 | method: 'GET', 678 | prefix: false, 679 | path: buildApiUrl(apiUrl, '/user/preferences'), 680 | options: { 681 | header: { 682 | 'Content-Type': 'text/xml' 683 | } 684 | } 685 | }, 686 | auth 687 | ).then(txt => xmlToJson(txt)); 688 | } 689 | 690 | /** 691 | * Request to set all preferences for a connected user 692 | * @param {osmAuth} auth An instance of osm-auth 693 | * @param {string} apiUrl The API URL 694 | * @param {Object} object An object to provide keys values to create XML preferences 695 | * @return {Promise} Promise 696 | */ 697 | export function setUserPreferencesRequest(auth, apiUrl, object) { 698 | const preferencesXml = buildPreferencesFromObjectXml(object); 699 | return authxhr( 700 | { 701 | method: 'PUT', 702 | prefix: false, 703 | path: buildApiUrl(apiUrl, '/user/preferences'), 704 | options: { 705 | header: { 706 | 'Content-Type': 'text/xml' 707 | } 708 | }, 709 | content: preferencesXml 710 | }, 711 | auth 712 | ); 713 | } 714 | 715 | /** 716 | * Request to fetch a preference from a key for the connected user 717 | * @param {osmAuth} auth An instance of osm-auth 718 | * @param {string} apiUrl The API URL 719 | * @param {string} key The key to retrieve 720 | * @throws Will throw an error for any request with http code 40x. 721 | * @return {Promise} Promise with the value for the key 722 | */ 723 | export function getUserPreferenceByKeyRequest(auth, apiUrl, key) { 724 | return authxhr( 725 | { 726 | method: 'GET', 727 | prefix: false, 728 | path: buildApiUrl(apiUrl, `/user/preferences/${key}`) 729 | }, 730 | auth 731 | ); 732 | } 733 | 734 | /** 735 | * Request to set a preference from a key for the connected user 736 | * @param {osmAuth} auth An instance of osm-auth 737 | * @param {string} apiUrl The API URL 738 | * @param {string} key The key to set 739 | * @param {string} value The value to set. Overwrite existing value if key exists 740 | * @return {Promise} Promise 741 | */ 742 | export function setUserPreferenceByKeyRequest(auth, apiUrl, key, value) { 743 | return authxhr( 744 | { 745 | method: 'PUT', 746 | prefix: false, 747 | path: buildApiUrl(apiUrl, `/user/preferences/${key}`), 748 | options: { 749 | header: { 750 | 'Content-Type': 'text/plain' 751 | } 752 | }, 753 | content: value 754 | }, 755 | auth 756 | ); 757 | } 758 | 759 | /** 760 | * Request to delete a preference from a key for the connected user 761 | * @param {osmAuth} auth An instance of osm-auth 762 | * @param {string} apiUrl The API URL 763 | * @param {string} key The key to use 764 | * @return {Promise} Promise 765 | */ 766 | export function deleteUserPreferenceRequest(auth, apiUrl, key) { 767 | return authxhr( 768 | { 769 | method: 'DELETE', 770 | prefix: false, 771 | path: buildApiUrl(apiUrl, `/user/preferences/${key}`) 772 | }, 773 | auth 774 | ); 775 | } 776 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../requests'); 2 | jest.mock('../helpers/time'); 3 | 4 | import defaultOptions from '../defaultOptions.json'; 5 | import OsmRequest from '../index'; 6 | 7 | const sampleNode = { 8 | $: { 9 | id: '3683625932', 10 | visible: 'true', 11 | version: '1', 12 | timestamp: '2015-08-06T09:49:47Z', 13 | changeset: '33150668', 14 | user: 'Vinber-Num&Lib', 15 | uid: '2568974', 16 | lat: '-0.5936602', 17 | lon: '44.8331455' 18 | }, 19 | tag: [], 20 | _id: '3683625932', 21 | _type: 'node' 22 | }; 23 | 24 | const sampleNodeNoTags = JSON.parse(JSON.stringify(sampleNode)); 25 | delete sampleNodeNoTags.tag; 26 | 27 | const sampleWay = { 28 | $: { 29 | id: '211323881', 30 | visible: 'true', 31 | version: '9', 32 | changeset: '65048894', 33 | timestamp: '2018-11-30T15:49:04Z', 34 | user: 'noyeux', 35 | uid: '4154080' 36 | }, 37 | nd: [ 38 | { 39 | $: { 40 | ref: '2213384362' 41 | } 42 | }, 43 | { 44 | $: { 45 | ref: '2179769628' 46 | } 47 | }, 48 | { 49 | $: { 50 | ref: '2179769632' 51 | } 52 | }, 53 | { 54 | $: { 55 | ref: '511563694' 56 | } 57 | }, 58 | { 59 | $: { 60 | ref: '511563688' 61 | } 62 | }, 63 | { 64 | $: { 65 | ref: '511563666' 66 | } 67 | }, 68 | { 69 | $: { 70 | ref: '511563658' 71 | } 72 | }, 73 | { 74 | $: { 75 | ref: '511563655' 76 | } 77 | }, 78 | { 79 | $: { 80 | ref: '511563646' 81 | } 82 | }, 83 | { 84 | $: { 85 | ref: '1425983435' 86 | } 87 | }, 88 | { 89 | $: { 90 | ref: '5370456212' 91 | } 92 | }, 93 | { 94 | $: { 95 | ref: '2032716031' 96 | } 97 | }, 98 | { 99 | $: { 100 | ref: '2032716064' 101 | } 102 | }, 103 | { 104 | $: { 105 | ref: '2032716087' 106 | } 107 | }, 108 | { 109 | $: { 110 | ref: '2894299077' 111 | } 112 | }, 113 | { 114 | $: { 115 | ref: '2357342688' 116 | } 117 | }, 118 | { 119 | $: { 120 | ref: '2173133206' 121 | } 122 | }, 123 | { 124 | $: { 125 | ref: '2173133198' 126 | } 127 | }, 128 | { 129 | $: { 130 | ref: '1979037083' 131 | } 132 | }, 133 | { 134 | $: { 135 | ref: '1979037078' 136 | } 137 | }, 138 | { 139 | $: { 140 | ref: '6106498823' 141 | } 142 | }, 143 | { 144 | $: { 145 | ref: '1979037077' 146 | } 147 | }, 148 | { 149 | $: { 150 | ref: '2179769629' 151 | } 152 | }, 153 | { 154 | $: { 155 | ref: '2213384362' 156 | } 157 | } 158 | ], 159 | tag: [ 160 | { 161 | $: { 162 | k: 'alt_name', 163 | v: "L'Estréniol" 164 | } 165 | }, 166 | { 167 | $: { 168 | k: 'landuse', 169 | v: 'retail' 170 | } 171 | }, 172 | { 173 | $: { 174 | k: 'name', 175 | v: 'Pôle commercial du Comtal Ouest' 176 | } 177 | }, 178 | { 179 | $: { 180 | k: 'old_name', 181 | v: "Zone Commercial l'Astragale" 182 | } 183 | }, 184 | { 185 | $: { 186 | k: 'wikipedia', 187 | v: 'fr:Le Comtal (Sébazac-Concourès)' 188 | } 189 | } 190 | ], 191 | _id: '211323881', 192 | _type: 'way' 193 | }; 194 | 195 | const sampleWayNoTags = JSON.parse(JSON.stringify(sampleWay)); 196 | delete sampleWayNoTags.tag; 197 | 198 | const sampleRelation = { 199 | $: { 200 | id: '2068206', 201 | visible: 'true', 202 | version: '2', 203 | changeset: '14958524', 204 | timestamp: '2013-02-08T18:11:06Z', 205 | user: 'isnogoud_bot', 206 | uid: '1220754' 207 | }, 208 | member: [ 209 | { 210 | $: { 211 | type: 'way', 212 | ref: '27847742', 213 | role: 'street' 214 | } 215 | }, 216 | { 217 | $: { 218 | type: 'node', 219 | ref: '1659643084', 220 | role: 'house' 221 | } 222 | }, 223 | { 224 | $: { 225 | type: 'node', 226 | ref: '1659643085', 227 | role: 'house' 228 | } 229 | }, 230 | { 231 | $: { 232 | type: 'node', 233 | ref: '1659643086', 234 | role: 'house' 235 | } 236 | }, 237 | { 238 | $: { 239 | type: 'node', 240 | ref: '1659643099', 241 | role: 'house' 242 | } 243 | }, 244 | { 245 | $: { 246 | type: 'node', 247 | ref: '1659643103', 248 | role: 'house' 249 | } 250 | }, 251 | { 252 | $: { 253 | type: 'node', 254 | ref: '1659643107', 255 | role: 'house' 256 | } 257 | }, 258 | { 259 | $: { 260 | type: 'node', 261 | ref: '1659643114', 262 | role: 'house' 263 | } 264 | }, 265 | { 266 | $: { 267 | type: 'node', 268 | ref: '1659643117', 269 | role: 'house' 270 | } 271 | }, 272 | { 273 | $: { 274 | type: 'node', 275 | ref: '1659643121', 276 | role: 'house' 277 | } 278 | }, 279 | { 280 | $: { 281 | type: 'node', 282 | ref: '1659643124', 283 | role: 'house' 284 | } 285 | }, 286 | { 287 | $: { 288 | type: 'node', 289 | ref: '1659643129', 290 | role: 'house' 291 | } 292 | }, 293 | { 294 | $: { 295 | type: 'node', 296 | ref: '1659643132', 297 | role: 'house' 298 | } 299 | }, 300 | { 301 | $: { 302 | type: 'node', 303 | ref: '1659643138', 304 | role: 'house' 305 | } 306 | }, 307 | { 308 | $: { 309 | type: 'node', 310 | ref: '1659643143', 311 | role: 'house' 312 | } 313 | }, 314 | { 315 | $: { 316 | type: 'node', 317 | ref: '1659643152', 318 | role: 'house' 319 | } 320 | }, 321 | { 322 | $: { 323 | type: 'node', 324 | ref: '1659643156', 325 | role: 'house' 326 | } 327 | }, 328 | { 329 | $: { 330 | type: 'node', 331 | ref: '1659643160', 332 | role: 'house' 333 | } 334 | }, 335 | { 336 | $: { 337 | type: 'node', 338 | ref: '1659643162', 339 | role: 'house' 340 | } 341 | }, 342 | { 343 | $: { 344 | type: 'node', 345 | ref: '1659643165', 346 | role: 'house' 347 | } 348 | }, 349 | { 350 | $: { 351 | type: 'node', 352 | ref: '1659643169', 353 | role: 'house' 354 | } 355 | }, 356 | { 357 | $: { 358 | type: 'node', 359 | ref: '1659643172', 360 | role: 'house' 361 | } 362 | }, 363 | { 364 | $: { 365 | type: 'node', 366 | ref: '1659643176', 367 | role: 'house' 368 | } 369 | }, 370 | { 371 | $: { 372 | type: 'node', 373 | ref: '1659643180', 374 | role: 'house' 375 | } 376 | }, 377 | { 378 | $: { 379 | type: 'node', 380 | ref: '1659643183', 381 | role: 'house' 382 | } 383 | }, 384 | { 385 | $: { 386 | type: 'node', 387 | ref: '1659643187', 388 | role: 'house' 389 | } 390 | }, 391 | { 392 | $: { 393 | type: 'node', 394 | ref: '1659643191', 395 | role: 'house' 396 | } 397 | }, 398 | { 399 | $: { 400 | type: 'node', 401 | ref: '1659643192', 402 | role: 'house' 403 | } 404 | }, 405 | { 406 | $: { 407 | type: 'node', 408 | ref: '1659643196', 409 | role: 'house' 410 | } 411 | } 412 | ], 413 | tag: [ 414 | { 415 | $: { 416 | k: 'name', 417 | v: 'Rue de Belleville' 418 | } 419 | }, 420 | { 421 | $: { 422 | k: 'ref:FR:FANTOIR', 423 | v: '728' 424 | } 425 | }, 426 | { 427 | $: { 428 | k: 'type', 429 | v: 'associatedStreet' 430 | } 431 | } 432 | ], 433 | _id: '2068206', 434 | _type: 'relation' 435 | }; 436 | 437 | const sampleRelationNoTags = JSON.parse(JSON.stringify(sampleRelation)); 438 | delete sampleRelationNoTags.tag; 439 | 440 | const sampleUserPrefs = { 441 | "some-pref-1": "some-value-1", 442 | "some-pref-2": "some-value-2" 443 | }; 444 | 445 | describe('OsmRequest', () => { 446 | describe('Getters', () => { 447 | it('Should return a default apiUrl', () => { 448 | const osm = new OsmRequest(); 449 | expect(osm.apiUrl).toBe(defaultOptions.apiUrl); 450 | }); 451 | 452 | it('Should return a custom apiUrl', () => { 453 | const customApiUrl = 'https://my-custom-apiUrl/api/0.6'; 454 | const osm = new OsmRequest({ apiUrl: customApiUrl }); 455 | expect(osm.apiUrl).toBe(customApiUrl); 456 | }); 457 | }); 458 | 459 | describe('createNodeElement', () => { 460 | it('Should return a new element', () => { 461 | const lat = 1.234; 462 | const lon = -0.456; 463 | const tags = { 464 | aze: 'rty', 465 | uio: 'pqs' 466 | }; 467 | const osm = new OsmRequest(); 468 | const elementWithTags = osm.createNodeElement(lat, lon, tags); 469 | const elementWithoutTags = osm.createNodeElement(lat, lon); 470 | 471 | expect(elementWithTags).toMatchSnapshot(); 472 | expect(elementWithoutTags).toMatchSnapshot(); 473 | }); 474 | }); 475 | 476 | describe('createWayElement', () => { 477 | it('Should return a new way element', () => { 478 | const nodeIds = [ 479 | 'node/2213384362', 480 | 'node/2179769628', 481 | 'node/2179769632', 482 | 'node/511563694', 483 | 'node/511563688', 484 | 'node/511563666', 485 | 'node/511563658', 486 | 'node/511563655', 487 | 'node/511563646', 488 | 'node/1425983435', 489 | 'node/5370456212', 490 | 'node/2032716031', 491 | 'node/2032716064', 492 | 'node/2032716087', 493 | 'node/2894299077', 494 | 'node/2357342688', 495 | 'node/2173133206', 496 | 'node/2173133198', 497 | 'node/1979037083', 498 | 'node/1979037078', 499 | 'node/6106498823', 500 | 'node/1979037077', 501 | 'node/2179769629', 502 | 'node/2213384362' 503 | ]; 504 | const tags = { 505 | aze: 'rty', 506 | uio: 'pqs' 507 | }; 508 | const osm = new OsmRequest(); 509 | const elementWithTags = osm.createWayElement(nodeIds, tags); 510 | const elementWithoutTags = osm.createWayElement(nodeIds); 511 | 512 | expect(elementWithTags).toMatchSnapshot(); 513 | expect(elementWithoutTags).toMatchSnapshot(); 514 | }); 515 | }); 516 | 517 | describe('createRelationElement', () => { 518 | it('Should return a new relation element', () => { 519 | const osmElementObjects = [ 520 | { 521 | role: 'street', 522 | id: 'way/27847742' 523 | }, 524 | { 525 | role: 'house', 526 | id: 'node/1659643084' 527 | }, 528 | { 529 | role: 'house', 530 | id: 'node/1659643085' 531 | }, 532 | { 533 | role: 'house', 534 | id: 'node/1659643086' 535 | }, 536 | { 537 | role: 'house', 538 | id: 'node/1659643099' 539 | }, 540 | { 541 | role: 'house', 542 | id: 'node/1659643103' 543 | }, 544 | { 545 | role: 'house', 546 | id: 'node/1659643107' 547 | }, 548 | { 549 | role: 'house', 550 | id: 'node/1659643114' 551 | }, 552 | { 553 | role: 'house', 554 | id: 'node/1659643117' 555 | }, 556 | { 557 | role: 'house', 558 | id: 'node/1659643121' 559 | }, 560 | { 561 | role: 'house', 562 | id: 'node/1659643124' 563 | }, 564 | { 565 | role: 'house', 566 | id: 'node/1659643129' 567 | }, 568 | { 569 | role: 'house', 570 | id: 'node/1659643132' 571 | }, 572 | { 573 | role: 'house', 574 | id: 'node/1659643138' 575 | }, 576 | { 577 | role: 'house', 578 | id: 'node/1659643143' 579 | }, 580 | { 581 | role: 'house', 582 | id: 'node/1659643152' 583 | }, 584 | { 585 | role: 'house', 586 | id: 'node/1659643156' 587 | }, 588 | { 589 | role: 'house', 590 | id: 'node/1659643160' 591 | }, 592 | { 593 | role: 'house', 594 | id: 'node/1659643162' 595 | }, 596 | { 597 | role: 'house', 598 | id: 'node/1659643165' 599 | }, 600 | { 601 | role: 'house', 602 | id: 'node/1659643169' 603 | }, 604 | { 605 | role: 'house', 606 | id: 'node/1659643172' 607 | }, 608 | { 609 | role: 'house', 610 | id: 'node/1659643176' 611 | }, 612 | { 613 | role: 'house', 614 | id: 'node/1659643180' 615 | }, 616 | { 617 | role: 'house', 618 | id: 'node/1659643183' 619 | }, 620 | { 621 | role: 'house', 622 | id: 'node/1659643187' 623 | }, 624 | { 625 | role: 'house', 626 | id: 'node/1659643191' 627 | }, 628 | { 629 | role: 'house', 630 | id: 'node/1659643192' 631 | }, 632 | { 633 | role: 'house', 634 | id: 'node/1659643196' 635 | } 636 | ]; 637 | const tags = { 638 | aze: 'rty', 639 | uio: 'pqs' 640 | }; 641 | const osm = new OsmRequest(); 642 | const elementWithTags = osm.createRelationElement( 643 | osmElementObjects, 644 | tags 645 | ); 646 | const elementWithoutTags = osm.createRelationElement(osmElementObjects); 647 | 648 | expect(elementWithTags).toMatchSnapshot(); 649 | expect(elementWithoutTags).toMatchSnapshot(); 650 | }); 651 | }); 652 | describe('getTags', () => { 653 | it('returns tags of node with tag property', () => { 654 | const osm = new OsmRequest(); 655 | const tags = osm.getTags(sampleNode); 656 | expect(tags).toMatchSnapshot(); 657 | }); 658 | 659 | it('returns tags of node without tag property', () => { 660 | const osm = new OsmRequest(); 661 | const tags = osm.getTags(sampleNodeNoTags); 662 | expect(tags).toMatchSnapshot(); 663 | }); 664 | 665 | it('returns tags of way with tag property', () => { 666 | const osm = new OsmRequest(); 667 | const tags = osm.getTags(sampleWay); 668 | expect(tags).toMatchSnapshot(); 669 | }); 670 | }); 671 | describe('setProperty', () => { 672 | it('has the same behavior as setTag', () => { 673 | const osm = new OsmRequest(); 674 | const tagName = 'weird_key'; 675 | const tagValue = 'stuff'; 676 | const element = osm.setProperty(sampleNode, tagName, tagValue); 677 | const expected = osm.setTag(sampleNode, tagName, tagValue); 678 | 679 | expect(element).toEqual(expected); 680 | }); 681 | }); 682 | describe('setTag', () => { 683 | it('Should add a tag to an element', () => { 684 | const osm = new OsmRequest(); 685 | const tagName = 'weird_key'; 686 | const tagValue = 'stuff'; 687 | const element = osm.setTag(sampleNode, tagName, tagValue); 688 | 689 | expect(element).toMatchSnapshot(); 690 | }); 691 | 692 | it('Should add a tag to an element having no tag', () => { 693 | const osm = new OsmRequest(); 694 | const tagName = 'weird_key'; 695 | const tagValue = 'stuff'; 696 | const element = osm.setTag(sampleNodeNoTags, tagName, tagValue); 697 | 698 | expect(element).toMatchSnapshot(); 699 | }); 700 | 701 | it('Should modify an element tag', () => { 702 | const osm = new OsmRequest(); 703 | const tagName = 'amenity'; 704 | const tagValue = 'stuff'; 705 | const element = osm.setTag(sampleNode, tagName, tagValue); 706 | 707 | expect(element).toMatchSnapshot(); 708 | }); 709 | }); 710 | 711 | describe('setProperties', () => { 712 | it('Should have the same behavior as setTags', () => { 713 | const osm = new OsmRequest(); 714 | const tagName = 'weird_key'; 715 | const tagValue = 'stuff'; 716 | const element = osm.setProperties(sampleNode, { 717 | [tagName]: tagValue 718 | }); 719 | const expected = osm.setTags(sampleNode, { 720 | [tagName]: tagValue 721 | }); 722 | expect(element).toEqual(expected); 723 | }); 724 | }); 725 | describe('setTags', () => { 726 | it('Should add a tag to an element', () => { 727 | const osm = new OsmRequest(); 728 | const tagName = 'weird_key'; 729 | const tagValue = 'stuff'; 730 | const element = osm.setTags(sampleNode, { 731 | [tagName]: tagValue 732 | }); 733 | 734 | expect(element).toMatchSnapshot(); 735 | }); 736 | 737 | it('Should modify an element tag', () => { 738 | const osm = new OsmRequest(); 739 | const tagName = 'amenity'; 740 | const tagValue = 'stuff'; 741 | const element = osm.setTags(sampleNode, { 742 | [tagName]: tagValue 743 | }); 744 | 745 | expect(element).toMatchSnapshot(); 746 | }); 747 | 748 | it('Should work with an element having no tags', () => { 749 | const osm = new OsmRequest(); 750 | const tagName = 'amenity'; 751 | const tagValue = 'stuff'; 752 | const element = osm.setTags(sampleNodeNoTags, { 753 | [tagName]: tagValue 754 | }); 755 | 756 | expect(element).toMatchSnapshot(); 757 | }); 758 | }); 759 | 760 | describe('replaceTags', () => { 761 | it('Should completely replace existing tags', () => { 762 | const osm = new OsmRequest(); 763 | const element = osm.replaceTags(sampleWay, { 764 | amenity: 'restaurant', 765 | name: 'Best Sushi in Town' 766 | }); 767 | 768 | expect(element).toMatchSnapshot(); 769 | }); 770 | }); 771 | describe('removeProperty', () => { 772 | it('Should remove a tag from an element', () => { 773 | const osm = new OsmRequest(); 774 | const tagName = 'amenity'; 775 | const expected = osm.removeTag(sampleNode, tagName); 776 | const element = osm.removeProperty(sampleNode, tagName); 777 | 778 | expect(element).toEqual(expected); 779 | }); 780 | }); 781 | 782 | describe('removeTag', () => { 783 | it('Should remove a tag from an element', () => { 784 | const osm = new OsmRequest(); 785 | const tagName = 'amenity'; 786 | const element = osm.removeTag(sampleNode, tagName); 787 | 788 | expect(element).toMatchSnapshot(); 789 | }); 790 | }); 791 | 792 | describe('setCoordinates', () => { 793 | it('Should update the coordinates of an element', () => { 794 | const lat = 1.234; 795 | const lon = -0.456; 796 | const osm = new OsmRequest(); 797 | const element = osm.setCoordinates(sampleNode, lat, lon); 798 | 799 | expect(element).toMatchSnapshot(); 800 | }); 801 | }); 802 | 803 | describe('getRelationMembers', () => { 804 | it('Should return the members of a relation', () => { 805 | const osm = new OsmRequest(); 806 | const members = osm.getRelationMembers(sampleRelation); 807 | 808 | expect(members).toMatchSnapshot(); 809 | }); 810 | }); 811 | 812 | describe('setRelationMembers', () => { 813 | it('Should update the members of a relation', () => { 814 | const osm = new OsmRequest(); 815 | const members = [ 816 | { 817 | role: 'street', 818 | id: 'way/27847742' 819 | }, 820 | { 821 | role: 'house', 822 | id: 'node/1659643084' 823 | } 824 | ]; 825 | const element = osm.setRelationMembers(sampleRelation, members); 826 | 827 | expect(element).toMatchSnapshot(); 828 | }); 829 | }); 830 | 831 | describe('setTimestampToNow', () => { 832 | it('Should update the timestamp of an element', () => { 833 | const osm = new OsmRequest(); 834 | const element = osm.setTimestampToNow(sampleNode); 835 | 836 | expect(element).toMatchSnapshot(); 837 | }); 838 | }); 839 | 840 | describe('setVersion', () => { 841 | it('Should change the version number of an element', () => { 842 | const osm = new OsmRequest(); 843 | const element = osm.setVersion(sampleNode, 3); 844 | 845 | expect(element).toMatchSnapshot(); 846 | }); 847 | }); 848 | 849 | describe('fetchElement', () => { 850 | it('Should fetch an element and returned its JSON representation', () => { 851 | const osm = new OsmRequest(); 852 | const element = osm.fetchElement(1234); 853 | 854 | expect(element).toMatchSnapshot(); 855 | }); 856 | }); 857 | 858 | describe('fetchMultipleElements', () => { 859 | it('Should fetch several elements and return their JSON representations', () => { 860 | const osm = new OsmRequest(); 861 | const element = osm.fetchMultipleElements([ 862 | 'node/123', 863 | 'node/456', 864 | 'node/678' 865 | ]); 866 | 867 | expect(element).toMatchSnapshot(); 868 | }); 869 | }); 870 | 871 | describe('fetchWaysForNode', () => { 872 | it('Should fetch ways using this node and return their JSON representation', () => { 873 | const osm = new OsmRequest(); 874 | const ways = osm.fetchWaysForNode('node/5336441517'); 875 | 876 | expect(ways).toMatchSnapshot(); 877 | }); 878 | }); 879 | 880 | describe('fetchNotes', () => { 881 | it('Should fetch notes from a given bbox', () => { 882 | const osm = new OsmRequest(); 883 | const notes = osm.fetchNotes(0, 0, 1, 1); 884 | 885 | expect(notes).toMatchSnapshot(); 886 | }); 887 | }); 888 | 889 | describe('fetchNotesSearch', () => { 890 | it('Should fetch notes using text search', () => { 891 | const osm = new OsmRequest(); 892 | const notes = osm.fetchNotesSearch('hydrant', 'json', 3); 893 | 894 | expect(notes).toMatchSnapshot(); 895 | }); 896 | }); 897 | 898 | describe('fetchNote', () => { 899 | it('Should fetch a single note by id', () => { 900 | const osm = new OsmRequest(); 901 | const notes = osm.fetchNote('1781930', 'json'); 902 | 903 | expect(notes).toMatchSnapshot(); 904 | }); 905 | }); 906 | 907 | describe('createNote', () => { 908 | it('Should return a new note XML', () => { 909 | const lat = 1.234; 910 | const lon = -0.456; 911 | const text = 'there is a problem here'; 912 | 913 | const osm = new OsmRequest(); 914 | const newnote = osm.createNote(lat, lon, text); 915 | 916 | expect(newnote).toMatchSnapshot(); 917 | }); 918 | }); 919 | 920 | describe('fetchMapByBbox', () => { 921 | it('Should fetch map elements for a given bbox', () => { 922 | const osm = new OsmRequest(); 923 | const osmElements = osm.fetchMapByBbox( 924 | -1.78859, 925 | 48.22076, 926 | -1.78784, 927 | 48.22122 928 | ); 929 | 930 | expect(osmElements).toMatchSnapshot(); 931 | }); 932 | }); 933 | 934 | describe('fetchUser', () => { 935 | it('Should fetch a single user by id', () => { 936 | const osm = new OsmRequest(); 937 | const user = osm.fetchUser('214436'); 938 | 939 | expect(user).toMatchSnapshot(); 940 | }); 941 | }); 942 | 943 | describe('getAndSetAllUserPreferences', () => { 944 | it('Should set then get user preferences', () => { 945 | const osm = new OsmRequest(); 946 | const res = osm.setUserPreferences(sampleUserPrefs); 947 | expect(res).toMatchSnapshot(); 948 | const prefs = osm.getUserPreferences(); 949 | expect(prefs).toMatchSnapshot(); 950 | }); 951 | }); 952 | 953 | describe('getAndSetUserPreferenceByKey', () => { 954 | it('Should fetch a single user preference by key', () => { 955 | const osm = new OsmRequest(); 956 | const res = osm.setUserPreferenceByKey('some-test-preference', 'some-value'); 957 | expect(res).toMatchSnapshot(); 958 | const pref = osm.getUserPreferenceByKey('some-test-preference'); 959 | expect(pref).toMatchSnapshot(); 960 | }); 961 | }); 962 | 963 | describe('deleteUserPreference', () => { 964 | it('Should delete a single user preference by key', () => { 965 | const osm = new OsmRequest(); 966 | const pref = osm.getUserPreferenceByKey('some-test-preference'); 967 | expect(pref).toMatchSnapshot(); 968 | const res = osm.deleteUserPreference('some-test-preference'); 969 | expect(res).toMatchSnapshot(); 970 | const prefs = osm.getUserPreferences(); 971 | expect(prefs).toMatchSnapshot(); 972 | }); 973 | }); 974 | 975 | describe('createChangeset', () => { 976 | it('Should return the changeset ID', () => { 977 | const osm = new OsmRequest(); 978 | const changesetid = osm.createChangeset('my editor', 'some comment', { 979 | tag: true 980 | }); 981 | 982 | expect(changesetid).toBeGreaterThan(0); 983 | }); 984 | }); 985 | 986 | describe('isChangesetStillOpen', () => { 987 | it('Should return true if open', () => { 988 | const osm = new OsmRequest(); 989 | const changesetid = osm.isChangesetStillOpen(1234); 990 | 991 | expect(changesetid).toBeTruthy(); 992 | }); 993 | }); 994 | 995 | describe('fetchChangeset', () => { 996 | it('Should return changeset details', () => { 997 | const osm = new OsmRequest(); 998 | const changesetid = osm.fetchChangeset(1234); 999 | 1000 | expect(changesetid).toMatchSnapshot(); 1001 | }); 1002 | }); 1003 | 1004 | describe('fetchChangesets', () => { 1005 | it('Should return several changesets details', () => { 1006 | const osm = new OsmRequest(); 1007 | const changesets = osm.fetchChangesets({ 1008 | left: -1.7883789539337158, 1009 | bottom: 48.22059295273195, 1010 | right: -1.7867642641067505, 1011 | top: 48.221488262276665 1012 | }); 1013 | 1014 | expect(changesets).toMatchSnapshot(); 1015 | }); 1016 | }); 1017 | }); 1018 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { osmAuth } from 'osm-auth'; 2 | import defaultOptions from './defaultOptions.json'; 3 | import { getCurrentIsoTimestamp } from 'helpers/time'; 4 | import { 5 | findElementType, 6 | findElementId, 7 | removeTrailingSlashes, 8 | simpleObjectDeepClone 9 | } from 'helpers/utils'; 10 | import { 11 | fetchElementRequest, 12 | fetchElementRequestFull, 13 | multiFetchElementsByTypeRequest, 14 | fetchMapByBboxRequest, 15 | fetchRelationsForElementRequest, 16 | fetchWaysForNodeRequest, 17 | sendElementRequest, 18 | fetchNotesRequest, 19 | fetchNotesSearchRequest, 20 | fetchNoteByIdRequest, 21 | createNoteRequest, 22 | genericPostNoteRequest, 23 | createChangesetRequest, 24 | changesetCheckRequest, 25 | changesetGetRequest, 26 | updateChangesetTagsRequest, 27 | closeChangesetRequest, 28 | uploadChangesetOscRequest, 29 | fetchChangesetsRequest, 30 | deleteElementRequest, 31 | fetchUserRequest, 32 | getUserPreferencesRequest, 33 | setUserPreferencesRequest, 34 | getUserPreferenceByKeyRequest, 35 | setUserPreferenceByKeyRequest, 36 | deleteUserPreferenceRequest 37 | } from './requests'; 38 | 39 | /** 40 | * OSM API request handler 41 | * @type {Object} 42 | */ 43 | export default class OsmRequest { 44 | /** 45 | * @access public 46 | * @param {Object} [options] Custom options to apply 47 | * @param {string} [options.scope] Scopes separated by a space, see https://wiki.openstreetmap.org/wiki/OAuth#OAuth_2.0 for all scopes (defaults to read_prefs write_prefs write_api write_notes) 48 | * @param {string} [options.client_id] The Client ID of your app. To register an app, see https://wiki.openstreetmap.org/wiki/OAuth#OAuth_2.0_2 49 | * @param {string} [options.redirect_uri] URL of your app (or url of app code) to rederict to after authentication. 50 | * @param {string} [options.access_token] Optional. OAuth2 bearer token to pre-authorize your app. 51 | * @param {string} [options.url] URL of the OSM server to use (defaults to https://openstreetmap.org) 52 | * @param {string} [options.apiUrl] URL of the OSM API to use (defaults to https://api.openstreetmap.org) 53 | * @param {boolean} [options.auto] If true, attempt to authenticate automatically when calling .xhr() or fetch() (default: false) 54 | * @param {Object} [options.singlepage] If true, use page redirection instead of a popup (default: false) 55 | * @param {string} [options.loading] Function called when auth-related xhr calls start 56 | * @param {string} [options.done] Function called when auth-related xhr calls end 57 | */ 58 | constructor(options = {}) { 59 | this._options = { 60 | ...defaultOptions, 61 | ...options 62 | }; 63 | 64 | this._options.url = removeTrailingSlashes(this._options.url); 65 | this._options.apiUrl = removeTrailingSlashes(this._options.apiUrl); 66 | this._auth = osmAuth({ 67 | scope: this._options.scope, 68 | client_id: this._options.client_id, 69 | url: this._options.url, 70 | apiUrl: this._options.apiUrl, 71 | redirect_uri: this._options.redirect_uri, 72 | access_token: this._options.access_token, 73 | auto: this._options.auto, 74 | singlepage: this._options.singlepage, 75 | loading: this._options.loading, 76 | done: this._options.done, 77 | locale: this._options.locale 78 | }); 79 | } 80 | 81 | /** 82 | * Return the API URL to use for the requests 83 | * @return {string} URL of the API 84 | */ 85 | get apiUrl() { 86 | return this._options.apiUrl; 87 | } 88 | 89 | /** 90 | * Retrieve the OSM notes in given bounding box 91 | * @param {number} left The minimal longitude (X) 92 | * @param {number} bottom The minimal latitude (Y) 93 | * @param {number} right The maximal longitude (X) 94 | * @param {number} top The maximal latitude (Y) 95 | * @param {number} [limit] The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) 96 | * @param {number} [closedDays] The amount of days a note needs to be closed to no longer be returned (defaults to 7, 0 means only opened notes are returned, and -1 means all notes are returned) 97 | * @return {Promise} Resolves on notes list 98 | */ 99 | fetchNotes(left, bottom, right, top, limit = null, closedDays = null) { 100 | return fetchNotesRequest( 101 | this.apiUrl, 102 | left, 103 | bottom, 104 | right, 105 | top, 106 | limit, 107 | closedDays, 108 | this._options.always_authenticated ? { auth: this._auth } : {} 109 | ); 110 | } 111 | 112 | /** 113 | * Fetch OSM notes with textual search 114 | * @param {string} q Specifies the search query 115 | * @param {string} [format] It can be 'xml' (default) to get OSM 116 | * and convert to JSON, 'raw' to return raw OSM XML, 'json' to 117 | * return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS 118 | * @param {number} [limit] The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) 119 | * @param {number} [closed] The amount of days a note needs to be closed to no longer be returned (defaults to 7, 0 means only opened notes are returned, and -1 means all notes are returned) 120 | * @param {string} [display_name] Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter 121 | * @param {number} [user] Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display_name parameter 122 | * @param {number} [from] Specifies the beginning of a date range to search in for a note 123 | * @param {number} [to] Specifies the end of a date range to search in for a note. Today date is the default 124 | * @return {Promise} 125 | */ 126 | fetchNotesSearch( 127 | q, 128 | format = 'xml', 129 | limit = null, 130 | closed = null, 131 | display_name = null, 132 | user = null, 133 | from = null, 134 | to = null 135 | ) { 136 | return fetchNotesSearchRequest( 137 | this.apiUrl, 138 | q, 139 | format, 140 | limit, 141 | closed, 142 | display_name, 143 | user, 144 | from, 145 | to, 146 | this._options.always_authenticated ? { auth: this._auth } : {} 147 | ); 148 | } 149 | 150 | /** 151 | * Get OSM note by id 152 | * param {number} noteId Identifier for the note 153 | * @param {string} format It can be 'xml' (default) to get OSM 154 | * and convert to JSON, 'raw' to return raw OSM XML, 'json' to 155 | * return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS 156 | * @return {Promise} 157 | */ 158 | fetchNote(noteId, format = 'xml') { 159 | return fetchNoteByIdRequest( 160 | this.apiUrl, 161 | noteId, 162 | format, 163 | this._options.always_authenticated ? { auth: this._auth } : {} 164 | ); 165 | } 166 | 167 | /** 168 | * Create an OSM note 169 | * @param {number} lat Specifies the latitude of the note 170 | * @param {number} lon Specifies the longitude of the note 171 | * @param {string} text A mandatory text field with arbitrary text containing the note 172 | * @return {Promise} 173 | */ 174 | createNote(lat, lon, text) { 175 | return createNoteRequest(this._auth, this.apiUrl, lat, lon, text); 176 | } 177 | 178 | /** 179 | * Comment an OSM note 180 | * @param {string} text A mandatory text field with arbitrary text containing the note 181 | * @return {Promise} 182 | */ 183 | commentNote(noteId, text) { 184 | return genericPostNoteRequest( 185 | this._auth, 186 | this.apiUrl, 187 | noteId, 188 | text, 189 | 'comment' 190 | ); 191 | } 192 | 193 | /** 194 | * Close an OSM note 195 | * @param {string} text A mandatory text field with arbitrary text containing the note 196 | * @return {Promise} 197 | */ 198 | closeNote(noteId, text) { 199 | return genericPostNoteRequest( 200 | this._auth, 201 | this.apiUrl, 202 | noteId, 203 | text, 204 | 'close' 205 | ); 206 | } 207 | 208 | /** 209 | * Reopen an OSM note 210 | * @param {string} text A mandatory text field with arbitrary text containing the note 211 | * @return {Promise} 212 | */ 213 | reopenNote(noteId, text) { 214 | return genericPostNoteRequest( 215 | this._auth, 216 | this.apiUrl, 217 | noteId, 218 | text, 219 | 'reopen' 220 | ); 221 | } 222 | 223 | /** 224 | * Send a request to OSM to create a new changeset 225 | * @param {string} [createdBy] 226 | * @param {string} [comment] 227 | * @param {string} [tags] 228 | * @return {Promise} 229 | */ 230 | createChangeset(createdBy = '', comment = '', tags = {}) { 231 | return createChangesetRequest( 232 | this._auth, 233 | this.apiUrl, 234 | createdBy, 235 | comment, 236 | tags 237 | ); 238 | } 239 | 240 | /** 241 | * Check if a changeset is still open 242 | * @param {number} changesetId 243 | * @return {Promise} 244 | */ 245 | isChangesetStillOpen(changesetId) { 246 | return changesetCheckRequest( 247 | this.apiUrl, 248 | changesetId, 249 | this._options.always_authenticated ? { auth: this._auth } : {} 250 | ); 251 | } 252 | 253 | /** 254 | * Get a changeset for a given id 255 | * @param {number} changesetId 256 | * @return {Promise} 257 | */ 258 | fetchChangeset(changesetId) { 259 | return changesetGetRequest( 260 | this.apiUrl, 261 | changesetId, 262 | this._options.always_authenticated ? { auth: this._auth } : {} 263 | ); 264 | } 265 | 266 | /** 267 | * Update changeset tags if still open 268 | * @param {number} changesetId 269 | * @param {string} [createdBy] 270 | * @param {string} [comment] 271 | * @param {Object} [object] use to set multiples tags 272 | * @throws Will throw an error for any request with http code 40x 273 | * @return {Promise} 274 | */ 275 | updateChangesetTags(changesetId, createdBy = '', comment = '', object = {}) { 276 | return updateChangesetTagsRequest( 277 | this._auth, 278 | this.apiUrl, 279 | changesetId, 280 | createdBy, 281 | comment, 282 | object 283 | ); 284 | } 285 | 286 | /** 287 | * Close changeset for a given id if still opened 288 | * @param {number} changesetId 289 | * @throws Will throw an error for any request with http code 40x. 290 | * @return {Promise} Empty string if it works 291 | */ 292 | closeChangeset(changesetId) { 293 | return closeChangesetRequest(this._auth, this.apiUrl, changesetId); 294 | } 295 | 296 | /** 297 | * Upload an OSC file content conforming to the OsmChange specification OSM changeset 298 | * @param {string} changesetId 299 | * @param {string} osmChangeContent OSC file content text 300 | * @throws Will throw an error for any request with http code 40x. 301 | * @return {Promise} 302 | */ 303 | uploadChangesetOsc(changesetId, osmChangeContent) { 304 | return uploadChangesetOscRequest( 305 | this._auth, 306 | this.apiUrl, 307 | changesetId, 308 | osmChangeContent 309 | ); 310 | } 311 | 312 | /** 313 | * Fetch changesets from OSM API 314 | * @param {Object} options Optional parameters 315 | * @param {number} [options.left] The minimal longitude (X) 316 | * @param {number} [options.bottom] The minimal latitude (Y) 317 | * @param {number} [options.right] The maximal longitude (X) 318 | * @param {number} [options.top] The maximal latitude (Y) 319 | * @param {string} [options.display_name] Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter 320 | * @param {number} [options.user] Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display_name parameter 321 | * @param {string} [options.time] Can be a unique value T1 or two values T1, T2 comma separated. Find changesets closed after value T1 or find changesets that were closed after T1 and created before T2. In other words, any changesets that were open at some time during the given time range T1 to T2. Time format is anything that http://ruby-doc.org/stdlib-2.6.3/libdoc/date/rdoc/DateTime.html#method-c-parse can parse. 322 | * @param {number} [options.open] Only finds changesets that are still open but excludes changesets that are closed or have reached the element limit for a changeset (50.000 at the moment). Can be set to true 323 | * @param {number} [options.closed] Only finds changesets that are closed or have reached the element limit. Can be set to true 324 | * @param {number} [options.changesets] Finds changesets with the specified ids 325 | * @return {Promise} 326 | */ 327 | fetchChangesets(options) { 328 | return fetchChangesetsRequest(this.apiUrl, { 329 | auth: this._options.always_authenticated ? this._auth : null, 330 | ...options 331 | }); 332 | } 333 | 334 | /** 335 | * Create a shiny new OSM node element, in a JSON format 336 | * @param {number} lat 337 | * @param {number} lon 338 | * @param {Object} [tags] Optional, initial tags 339 | * @param {string} [id] Optional, identifier for OSM element 340 | * @return {Object} 341 | */ 342 | createNodeElement(lat, lon, tags = {}, id) { 343 | const node = { 344 | $: { 345 | lat: lat, 346 | lon: lon 347 | }, 348 | tag: [], 349 | _type: 'node' 350 | }; 351 | if (typeof id !== 'undefined') { 352 | node._id = id.toString(); 353 | } 354 | 355 | node.tag = Object.keys(tags).map(tagName => ({ 356 | $: { 357 | k: tagName.toString(), 358 | v: tags[tagName].toString() 359 | } 360 | })); 361 | 362 | return node; 363 | } 364 | 365 | /** 366 | * Create a shiny new OSM way element, in a JSON format 367 | * @param {Array