├── .nvmrc ├── .prettierrc ├── src ├── helpers │ ├── __mocks__ │ │ └── time.js │ ├── time.js │ ├── __tests__ │ │ ├── time.test.js │ │ ├── utils.test.js │ │ ├── xhr.test.js │ │ ├── xml.test.js │ │ └── __snapshots__ │ │ │ └── xml.test.js.snap │ ├── utils.js │ ├── xhr.js │ └── xml.js ├── exceptions │ ├── request.js │ └── __tests__ │ │ └── request.js ├── defaultOptions.json ├── __mocks__ │ ├── node.xml │ ├── user.xml │ ├── way_full.xml │ └── notes.xml ├── requests.js ├── __tests__ │ └── index.test.js └── index.js ├── .lintstagedrc.json ├── .babelrc ├── .eslintrc ├── __mocks__ └── cross-fetch.js ├── .gitignore ├── CHANGELOG.md ├── webpack.config.js ├── .github └── workflows │ └── build-tests-coverage.yml ├── LICENSE ├── package.json ├── README.md ├── jest.config.js └── API.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | trailingComma: "none" 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: true 5 | arrowParens: "avoid" 6 | printWidth: 80 7 | endOfLine: "auto" -------------------------------------------------------------------------------- /src/helpers/__mocks__/time.js: -------------------------------------------------------------------------------- 1 | export const getCurrentIsoTimestamp = jest 2 | .fn() 3 | .mockImplementation(() => '2018-04-25T07:06:47.550Z'); 4 | -------------------------------------------------------------------------------- /src/helpers/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the current timestamp (for testing purpose) 3 | * @return {string} 4 | */ 5 | export function getCurrentIsoTimestamp() { 6 | return new Date().toISOString(); 7 | } 8 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{js,json}": [ 3 | "prettier --config .prettierrc --write", 4 | "eslint" 5 | ], 6 | "src/**/*.js": [ 7 | "npm run test-ci -- --findRelatedTests", 8 | "npm run doc", 9 | "git add API.md" 10 | ] 11 | } -------------------------------------------------------------------------------- /src/exceptions/request.js: -------------------------------------------------------------------------------- 1 | export class RequestException extends Error { 2 | constructor(message) { 3 | super(message); 4 | if (Error.captureStackTrace) { 5 | Error.captureStackTrace(this, RequestException); 6 | } 7 | this.name = 'RequestException'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current", 8 | "browsers": "> 1%" 9 | }, 10 | "useBuiltIns": "entry", 11 | "corejs": 3 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "plugins": [ 4 | "import", 5 | "json", 6 | "jest" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "prettier" 11 | ], 12 | "env": { 13 | "es6": true, 14 | "browser": true, 15 | "node": true, 16 | "jest": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /__mocks__/cross-fetch.js: -------------------------------------------------------------------------------- 1 | module.exports = (url, opts) => { 2 | if (url && url.includes("notfound")) { 3 | return Promise.resolve({ status: 404, statusText: "Not Found", text: () => Promise.resolve("Mock cross-fetch error") }); 4 | } else { 5 | return Promise.resolve({ status: 200, statusText: "OK", text: () => Promise.resolve("Mock cross-fetch success") }); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/defaultOptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "read_prefs write_prefs write_api write_notes", 3 | "client_id": "", 4 | "redirect_uri": "", 5 | "access_token": "", 6 | "url": "https://www.openstreetmap.org", 7 | "apiUrl": "https://api.openstreetmap.org", 8 | "auto": false, 9 | "singlepage": false, 10 | "loading": null, 11 | "done": null, 12 | "locale": "" 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/helpers/__tests__/time.test.js: -------------------------------------------------------------------------------- 1 | import { getCurrentIsoTimestamp } from '../time'; 2 | 3 | describe('Time helpers', () => { 4 | describe('getCurrentIsoTimestamp', () => { 5 | it('works', () => { 6 | const result = getCurrentIsoTimestamp(); 7 | expect( 8 | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(result) 9 | ).toBeTruthy(); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.0.0] - 2024-07-01 8 | 9 | ### Added 10 | 11 | - Support of OAuth 2.0 via library osm-auth 2.5.0 12 | 13 | ### Removed 14 | 15 | - Support of OAuth 1.0 and basic auth -------------------------------------------------------------------------------- /src/exceptions/__tests__/request.js: -------------------------------------------------------------------------------- 1 | import { RequestException } from '../request'; 2 | 3 | const message = 'My message'; 4 | 5 | function failer() { 6 | throw new RequestException(message); 7 | } 8 | 9 | describe('RequestException', () => { 10 | it('Should correctly throw', () => { 11 | expect(failer).toThrow(); 12 | expect(failer).toThrow(message); 13 | expect(failer).toThrowError(new RequestException(message)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__mocks__/node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | plugins: [], 6 | devtool: 'cheap-module-source-map', 7 | entry: { 8 | OsmRequest: path.join(__dirname, 'src/index.js') 9 | }, 10 | output: { 11 | library: 'OsmRequest', 12 | libraryTarget: 'umd', 13 | libraryExport: 'default', 14 | filename: '[name].js', 15 | path: path.join(__dirname, 'dist') 16 | }, 17 | resolve: { 18 | modules: [ 19 | 'node_modules', 20 | process.env.NODE_PATH 21 | ], 22 | fallback: { 23 | "stream": false, 24 | "timers": false, 25 | "buffer": false, 26 | "string_decoder": false, 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.js$/, 33 | loader: 'babel-loader', 34 | exclude: /node_modules/ 35 | } 36 | ] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /.github/workflows/build-tests-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Build, Tests and Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | branches: 10 | - main 11 | - develop 12 | 13 | jobs: 14 | build-tests-coverage: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4.1.7 20 | 21 | - name: Setup Node.js environment 22 | uses: actions/setup-node@v4.0.2 23 | with: 24 | node-version: 20 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Build project 30 | run: npm run build 31 | 32 | - name: Run tests and generate coverage report 33 | run: npm run test-ci 34 | 35 | - name: Coveralls GitHub Action 36 | uses: coverallsapp/github-action@v2.3.0 37 | 38 | - name: Purge README 39 | uses: wow-actions/purge-readme@v1.0.0 40 | -------------------------------------------------------------------------------- /src/__mocks__/user.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenStreetMap passionate, I contribute mainly in the west of France on various subjects : indoor mapping, street equipment, advertisement... 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 OpenStreetMap developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osm-request", 3 | "description": "Request the OSM API from Javascript, with promises :)", 4 | "version": "2.0.0", 5 | "homepage": "https://github.com/osmlab/osm-request/", 6 | "repository": "https://github.com/osmlab/osm-request/", 7 | "bugs": "https://github.com/osmlab/osm-request/issues", 8 | "author": "OpenStreetMap developers", 9 | "license": "MIT", 10 | "keywords": [ 11 | "osm", 12 | "openstreetmap", 13 | "request", 14 | "api" 15 | ], 16 | "main": "dist/OsmRequest.js", 17 | "files": [ 18 | "dist" 19 | ], 20 | "moduleRoots": [ 21 | "node_modules", 22 | "src" 23 | ], 24 | "scripts": { 25 | "watch": "cross-env NODE_PATH=src webpack -w --progress", 26 | "build": "cross-env NODE_ENV=production NODE_PATH=src webpack --progress", 27 | "precommit": "lint-staged", 28 | "test": "cross-env NODE_PATH=src jest", 29 | "test-watch": "npm test -- --coverage --watch", 30 | "test-ci": "npm test -- --ci --coverage", 31 | "test-prettier": "prettier --config .prettierrc --list-different \"src/**/*.{js,json}\"", 32 | "lint": "eslint \"src/**/*.{js,json}\"", 33 | "doc": "npm run doc:lint && documentation build ./src/* -f md > API.md", 34 | "doc:lint": "documentation lint ./src/*", 35 | "preversion": "npm run test-ci && npm run build", 36 | "postversion": "git push && git push --tags" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.24.9", 40 | "@babel/eslint-parser": "^7.24.8", 41 | "@babel/preset-env": "^7.24.8", 42 | "babel-jest": "29.7.0", 43 | "babel-loader": "^9.1.3", 44 | "core-js": "^3.37.1", 45 | "coveralls": "^3.1.1", 46 | "cross-env": "^7.0.3", 47 | "documentation": "^14.0.3", 48 | "eslint": "^8.57.0", 49 | "eslint-config-prettier": "^9.1.0", 50 | "eslint-plugin-import": "^2.29.1", 51 | "eslint-plugin-jest": "^28.6.0", 52 | "eslint-plugin-json": "^4.0.0", 53 | "husky": "^9.0.11", 54 | "jest": "29.7.0", 55 | "jest-environment-jsdom": "29.7.0", 56 | "lint-staged": "^15.2.7", 57 | "prettier": "^3.3.3", 58 | "webpack": "^5.93.0", 59 | "webpack-cli": "^5.1.4" 60 | }, 61 | "dependencies": { 62 | "cross-fetch": "^4.0.0", 63 | "osm-auth": "^2.5.0", 64 | "xml2js": "^0.6.2", 65 | "xmlserializer": "^0.6.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/helpers/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove the trailing slashes from an URL and return it 3 | * @param {string} url 4 | * @return {string} - The cleaned URL 5 | */ 6 | export function removeTrailingSlashes(url) { 7 | return url.replace(/\/*$/, ''); 8 | } 9 | 10 | /** 11 | * Return a deep clone of an object. 12 | * 13 | * The object has to be a simple object notation. Not a Map, a Set or anything else. 14 | * 15 | * Object.assign and the spread operators can not be used as they do not replace children objects 16 | * by copies of themselves. 17 | * 18 | * Eg: If you use Object.assign or the rest operator on that object: { items: [{ childrenItem: 1 }] } 19 | * All the objects contained in the items array will be references to the first objects 20 | * @param {Object} object - A simple object notation. No Map, Set or anything 21 | * @return {Object} 22 | */ 23 | export function simpleObjectDeepClone(object) { 24 | return JSON.parse(JSON.stringify(object)); 25 | } 26 | 27 | /** 28 | * Return the type of an element based on the full OSM ID 29 | * @param {string} osmId 30 | * @return {string} 31 | */ 32 | export function findElementType(osmId) { 33 | return /^(\w+)\//.exec(osmId)[1]; 34 | } 35 | 36 | /** 37 | * Return the ID of an element based on the full OSM ID 38 | * @param {string} osmId 39 | * @return {string} 40 | */ 41 | export function findElementId(osmId) { 42 | return /\/(\d+)$/.exec(osmId)[1]; 43 | } 44 | 45 | /** 46 | * Check the OSM ID e.g -12 is negative 47 | * @param {string} id The ID (without type) 48 | * @return {boolean} 49 | */ 50 | export function checkIdIsNegative(id) { 51 | return /^[-]\d+$/.test(id); 52 | } 53 | 54 | /** 55 | * @param {Object} params 56 | * @return {string} 57 | */ 58 | export function buildQueryString(params) { 59 | if (params === null || typeof params === 'undefined') { 60 | return ''; 61 | } 62 | 63 | const builtParams = []; 64 | 65 | for (const paramName of Object.keys(params)) { 66 | const encodedName = encodeURIComponent(paramName); 67 | const encodedvalue = encodeURIComponent(params[paramName]); 68 | 69 | builtParams.push(`${encodedName}=${encodedvalue}`); 70 | } 71 | 72 | const questionMark = builtParams.length > 0 ? '?' : ''; 73 | const queryString = builtParams.join('&'); 74 | 75 | return `${questionMark}${queryString}`; 76 | } 77 | 78 | /** 79 | * Constructs complete API URL 80 | * @param {string} apiUrl The apiUrl URL 81 | * @param {string} path The method you want to use (example: /node/1234) 82 | * @param {Object} [params] The URL parameters 83 | * @return {string} The complete URL 84 | */ 85 | export function buildApiUrl(apiUrl, path, params) { 86 | return `${removeTrailingSlashes(apiUrl)}/api/0.6${path}${buildQueryString( 87 | params 88 | )}`; 89 | } 90 | -------------------------------------------------------------------------------- /src/__mocks__/way_full.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/helpers/xhr.js: -------------------------------------------------------------------------------- 1 | import crossFetch from 'cross-fetch'; 2 | import serializer from 'xmlserializer'; 3 | import { RequestException } from '../exceptions/request'; 4 | 5 | /** 6 | * Wrapper for fetching data from a given URL. 7 | * Uses either simple fetch or authenticated xhr according to specified options. 8 | * @param {string} url The URL to call 9 | * @param {Object} [options] Options object 10 | * @param {boolean} [options.auth] If auth XHR object is passed, it will be used instead of a simple fetch 11 | * @return {Promise} Resolves on response text, or rejects if any HTTP error occurs 12 | */ 13 | export function fetch(url, options = {}) { 14 | if (options.auth) { 15 | return new Promise((resolve, reject) => { 16 | options.auth.xhr( 17 | { 18 | method: 'GET', 19 | prefix: false, 20 | path: url 21 | }, 22 | (err, res) => { 23 | if (err) { 24 | return reject( 25 | new RequestException( 26 | JSON.stringify({ 27 | message: `Request failed`, 28 | status: err.status, 29 | statusText: err.statusText 30 | }) 31 | ) 32 | ); 33 | } else { 34 | if (res instanceof XMLDocument) { 35 | return resolve(serializer.serializeToString(res)); 36 | } else { 37 | return resolve(res); 38 | } 39 | } 40 | } 41 | ); 42 | }); 43 | } else { 44 | return crossFetch(url) 45 | .then(response => { 46 | if (response.status !== 200) { 47 | return response.text().then(message => 48 | Promise.reject( 49 | JSON.stringify({ 50 | message, 51 | status: response.status, 52 | statusText: response.statusText 53 | }) 54 | ) 55 | ); 56 | } 57 | 58 | return response; 59 | }) 60 | .catch(error => { 61 | throw new RequestException(error); 62 | }) 63 | .then(response => response.text()); 64 | } 65 | } 66 | 67 | /** 68 | * Wrapper for authenticated XmlHttpRequest 69 | * Uses osm-auth xhr function 70 | * @param {Object} opts Options object 71 | * @param {Object} auth Auth module to use 72 | * @return {Promise} Resolves on response text, or rejects if any HTTP error occurs 73 | */ 74 | export function authxhr(opts, auth) { 75 | if (auth.xhr) { 76 | return new Promise((resolve, reject) => { 77 | auth.xhr(opts, (err, res) => { 78 | if (err) { 79 | return reject( 80 | new RequestException( 81 | JSON.stringify({ 82 | message: `Request failed`, 83 | status: err.status, 84 | statusText: err.statusText 85 | }) 86 | ) 87 | ); 88 | } else { 89 | if (res instanceof XMLDocument) { 90 | return resolve(serializer.serializeToString(res)); 91 | } else { 92 | return resolve(res); 93 | } 94 | } 95 | }); 96 | }); 97 | } else if (auth.skip) { 98 | if (opts.content) { 99 | opts.body = opts.content; 100 | } 101 | 102 | opts.headers = {}; 103 | if (opts.options && opts.options.header) { 104 | opts.headers = opts.options.header; 105 | } 106 | 107 | return crossFetch(opts.path, opts) 108 | .then(response => { 109 | if (response.status !== 200) { 110 | return response.text().then(message => 111 | Promise.reject( 112 | JSON.stringify({ 113 | message: message, 114 | status: response.status, 115 | statusText: response.statusText 116 | }) 117 | ) 118 | ); 119 | } 120 | 121 | return response; 122 | }) 123 | .catch(error => { 124 | throw new RequestException(error); 125 | }) 126 | .then(response => response.text()); 127 | } else { 128 | throw new Error('Authenticated XHR needs OAuth information'); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/__mocks__/notes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1369237 5 | https://api.openstreetmap.org/api/0.6/notes/1369237 6 | https://api.openstreetmap.org/api/0.6/notes/1369237/comment 7 | https://api.openstreetmap.org/api/0.6/notes/1369237/close 8 | 2018-04-22 17:54:49 UTC 9 | open 10 | 11 | 12 | 2018-04-22 17:54:49 UTC 13 | opened 14 | Chez Florentin, Faustine, Aimé, Julien et Marine. 15 | <p>Chez Florentin, Faustine, Aimé, Julien et Marine.</p> 16 | 17 | 18 | 2018-04-22 17:57:56 UTC 19 | commented 20 | c'est quoi ? une note personnelle ou le nom d'un établissement ? 21 | <p>c'est quoi ? une note personnelle ou le nom d'un établissement ?</p> 22 | 23 | 24 | 25 | 26 | 1270165 27 | https://api.openstreetmap.org/api/0.6/notes/1270165 28 | https://api.openstreetmap.org/api/0.6/notes/1270165/reopen 29 | 2018-01-16 15:28:34 UTC 30 | closed 31 | 2018-04-20 11:43:00 UTC 32 | 33 | 34 | 2018-01-16 15:28:34 UTC 35 | opened 36 | carrefour market 37 | <p>carrefour market</p> 38 | 39 | 40 | 2018-04-20 11:43:00 UTC 41 | 2533303 42 | opline 43 | https://api.openstreetmap.org/user/opline 44 | closed 45 | 46 | <p></p> 47 | 48 | 49 | 50 | 51 | 1106518 52 | https://api.openstreetmap.org/api/0.6/notes/1106518 53 | https://api.openstreetmap.org/api/0.6/notes/1106518/comment 54 | https://api.openstreetmap.org/api/0.6/notes/1106518/close 55 | 2017-08-16 15:41:56 UTC 56 | open 57 | 58 | 59 | 2017-08-16 15:41:56 UTC 60 | opened 61 | Monplaisir 62 | <p>Monplaisir</p> 63 | 64 | 65 | 2017-08-16 15:42:43 UTC 66 | commented 67 | Les Carbonnières 68 | 69 | <p>Les Carbonnières 70 | </p> 71 | 72 | 73 | 2017-08-17 16:57:49 UTC 74 | 379182 75 | Metaltyty 76 | https://api.openstreetmap.org/user/Metaltyty 77 | commented 78 | lieu-dit, rue ou résidence? 79 | <p>lieu-dit, rue ou résidence?</p> 80 | 81 | 82 | 2017-08-24 15:44:07 UTC 83 | commented 84 | Lieu dit 85 | <p>Lieu dit</p> 86 | 87 | 88 | 2017-10-22 16:06:12 UTC 89 | 2533303 90 | opline 91 | https://api.openstreetmap.org/user/opline 92 | commented 93 | Monplaisir ou les carbonnières ? Vous nous proposez deux noms 94 | <p>Monplaisir ou les carbonnières ? Vous nous proposez deux noms</p> 95 | 96 | 97 | 2018-04-08 19:35:56 UTC 98 | 2568974 99 | Vinber-Num&Lib 100 | https://api.openstreetmap.org/user/Vinber-Num&Lib 101 | commented 102 | on cloture, pas de réactions ? 103 | <p>on cloture, pas de réactions ?</p> 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/helpers/__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | removeTrailingSlashes, 3 | simpleObjectDeepClone, 4 | buildQueryString, 5 | findElementType, 6 | findElementId, 7 | buildApiUrl, 8 | checkIdIsNegative 9 | } from '../utils'; 10 | 11 | describe('Utils helpers', () => { 12 | describe('removeTrailingSlashes', () => { 13 | it('Should remove the trailing slash when it is present', () => { 14 | expect(removeTrailingSlashes('azertyuiop/')).toBe('azertyuiop'); 15 | expect(removeTrailingSlashes('http://azertyuiop/')).toBe( 16 | 'http://azertyuiop' 17 | ); 18 | }); 19 | 20 | it('Should not remove anything when the trainling slash is not present', () => { 21 | expect(removeTrailingSlashes('azertyuiop')).toBe('azertyuiop'); 22 | expect(removeTrailingSlashes('http://azertyuiop')).toBe( 23 | 'http://azertyuiop' 24 | ); 25 | expect(removeTrailingSlashes('http://azertyuiop/azertyuiop')).toBe( 26 | 'http://azertyuiop/azertyuiop' 27 | ); 28 | }); 29 | }); 30 | 31 | describe('findElementType', () => { 32 | it('Should find and return an OSM element type from a full OSM ID', () => { 33 | expect(findElementType('node/1234')).toBe('node'); 34 | expect(findElementType('way/4567')).toBe('way'); 35 | expect(findElementType('relation/890')).toBe('relation'); 36 | }); 37 | }); 38 | 39 | describe('findElementId', () => { 40 | it('Should find and return an OSM element ID from a full OSM ID', () => { 41 | expect(findElementId('node/1234')).toBe('1234'); 42 | expect(findElementId('way/4567')).toBe('4567'); 43 | expect(findElementId('relation/890')).toBe('890'); 44 | }); 45 | }); 46 | 47 | describe('simpleObjectDeepClone', () => { 48 | it('Should properly copy an object', () => { 49 | const sample = { 50 | item1: true, 51 | item2: false, 52 | item3: ['aze', 'rty', 'uio'], 53 | item4: [{ aze: 1, rty: 2, uio: 3 }] 54 | }; 55 | const result = simpleObjectDeepClone(sample); 56 | result.item1 = false; 57 | result.item2 = true; 58 | result.item3[0] = 'test'; 59 | result.item4[0].aze = 11; 60 | result.item4[0].rty = 22; 61 | result.item4[0].uio = 33; 62 | 63 | expect(sample).not.toEqual(result); 64 | expect(sample.item1).not.toBe(result.item1); 65 | expect(sample.item2).not.toBe(result.item2); 66 | expect(sample.item3).not.toBe(result.item3); 67 | expect(sample.item3[0]).not.toBe(result.item3[0]); 68 | expect(sample.item4).not.toBe(result.item4); 69 | expect(sample.item4).not.toEqual(result.item4); 70 | expect(sample.item4[0]).not.toBe(result.item4[0]); 71 | expect(sample.item4[0]).not.toEqual(result.item4[0]); 72 | }); 73 | }); 74 | 75 | describe('buildQueryString', () => { 76 | it('Should return a valid query string', () => { 77 | const params = { 78 | param1: 'stuff', 79 | 'param-2': 'stuff 2', 80 | param_3: 'stuff 3' 81 | }; 82 | const result = buildQueryString(params); 83 | const expected = '?param1=stuff¶m-2=stuff%202¶m_3=stuff%203'; 84 | 85 | expect(result).toBe(expected); 86 | }); 87 | it('Should return empty string if null passed', () => { 88 | expect(buildQueryString()).toBe(''); 89 | }); 90 | }); 91 | 92 | describe('buildApiUrl', () => { 93 | it('Should return valid URL', () => { 94 | const result = buildApiUrl('https://osm.org', '/node/1234'); 95 | const expected = 'https://osm.org/api/0.6/node/1234'; 96 | expect(result).toBe(expected); 97 | }); 98 | it('Should return a valid URL with parameters', () => { 99 | const result = buildApiUrl('https://osm.org', '/node/1234', { 100 | p: 1, 101 | key: true, 102 | tag: 'value' 103 | }); 104 | const expected = 105 | 'https://osm.org/api/0.6/node/1234?p=1&key=true&tag=value'; 106 | expect(result).toBe(expected); 107 | }); 108 | it('Should return valid URL with end slash', () => { 109 | const result = buildApiUrl('https://osm.org/', '/node/1234'); 110 | const expected = 'https://osm.org/api/0.6/node/1234'; 111 | expect(result).toBe(expected); 112 | }); 113 | }); 114 | 115 | describe('checkIdIsNegative', () => { 116 | it('is true when ID is negative', () => { 117 | const result = checkIdIsNegative('-1234'); 118 | expect(result).toBeTruthy(); 119 | }); 120 | it('is false when ID is not negative', () => { 121 | const result = checkIdIsNegative('1234'); 122 | expect(result).not.toBeTruthy(); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/helpers/__tests__/xhr.test.js: -------------------------------------------------------------------------------- 1 | import { fetch, authxhr } from '../xhr'; 2 | 3 | describe('XHR helpers', () => { 4 | describe('fetch', () => { 5 | it('works with cross-fetch (no auth)', async () => { 6 | const res = await fetch('https://whatev.er/file.json'); 7 | expect(res).toBeTruthy(); 8 | expect(typeof res).toBe('string'); 9 | expect(res.length).toBeGreaterThan(0); 10 | expect(res).toEqual('Mock cross-fetch success'); 11 | }); 12 | 13 | it('fetch handles non-200 status codes', async () => { 14 | try { 15 | await fetch('https://whatev.er/notfound.json'); 16 | throw new Error('Test failed because fetch did not throw an error'); 17 | } catch (error) { 18 | expect(error.name).toEqual('RequestException'); 19 | expect(error.message).toEqual( 20 | JSON.stringify({ 21 | message: 'Mock cross-fetch error', 22 | status: 404, 23 | statusText: 'Not Found' 24 | }) 25 | ); 26 | } 27 | }); 28 | 29 | it('works with OAuth', async () => { 30 | const expected = 'OK'; 31 | const res = await authxhr( 32 | { method: 'GET', path: 'https://whatev.er/file.json' }, 33 | { xhr: (opts, callback) => callback(null, expected) } 34 | ); 35 | expect(res).toBeTruthy(); 36 | expect(res).toEqual(expected); 37 | }); 38 | 39 | it('authxhr handles authentication error with non-200 status code', async () => { 40 | const opts = {}; 41 | const err = { status: 404, statusText: 'Not Found' }; 42 | const auth = { xhr: (opts, callback) => callback(err, null) }; 43 | 44 | try { 45 | await authxhr(opts, auth); 46 | throw new Error('Test failed because authxhr did not throw an error'); 47 | } catch (error) { 48 | expect(error.name).toEqual('RequestException'); 49 | expect(error.message).toEqual( 50 | JSON.stringify({ 51 | message: 'Request failed', 52 | status: err.status, 53 | statusText: err.statusText 54 | }) 55 | ); 56 | } 57 | }); 58 | 59 | it('throws RequestException when auth.xhr callback returns an error', async () => { 60 | const url = 'https://whatev.er/file.json'; 61 | const err = { status: 404, statusText: 'Not Found' }; 62 | const opts = { auth: { xhr: (opts, callback) => callback(err, null) } }; 63 | try { 64 | await fetch(url, opts); 65 | throw new Error('Test failed because fetch did not throw an error'); 66 | } catch (error) { 67 | expect(error.name).toEqual('RequestException'); 68 | expect(error.message).toEqual( 69 | JSON.stringify({ 70 | message: 'Request failed', 71 | status: err.status, 72 | statusText: err.statusText 73 | }) 74 | ); 75 | } 76 | }); 77 | 78 | it('should return the correct XML string when auth.xhr callback returns an XMLDocument', async () => { 79 | const url = 'https://whatev.er/file.json'; 80 | const xml = 'test'; 81 | const parser = new DOMParser(); 82 | const doc = parser.parseFromString(xml, 'application/xml'); 83 | const opts = { auth: { xhr: (opts, callback) => callback(null, doc) } }; 84 | 85 | const res = await fetch(url, opts); 86 | expect(res).toBeTruthy(); 87 | 88 | const serializer = new XMLSerializer(); 89 | const xmlString = serializer.serializeToString(res); 90 | 91 | expect(xmlString).toEqual(xml); 92 | }); 93 | }); 94 | 95 | describe('authxhr', () => { 96 | it('throws an error when basic auth is used', async () => { 97 | const opts = { method: 'GET', path: 'https://whatev.er/file.json' }; 98 | const auth = { basic: { user: 'User', pass: 'Pass' } }; 99 | 100 | expect(() => authxhr(opts, auth)).toThrow( 101 | 'Authenticated XHR needs OAuth information' 102 | ); 103 | }); 104 | 105 | it('works with skipped auth', async () => { 106 | const res = await authxhr( 107 | { method: 'GET', path: 'https://whatev.er/file.json' }, 108 | { skip: true } 109 | ); 110 | expect(res).toBeTruthy(); 111 | expect(res).toEqual('Mock cross-fetch success'); 112 | }); 113 | 114 | it('authxhr handles response with non-200 status code ', async () => { 115 | const content = 'default content'; 116 | const header = { 'Content-Type': 'application/json' }; 117 | const opts = { 118 | method: 'GET', 119 | path: 'https://whatev.er/notfound.json', 120 | content: content, 121 | options: { 122 | header: header 123 | } 124 | }; 125 | const auth = { skip: true }; 126 | try { 127 | await authxhr(opts, auth); 128 | throw new Error('Test failed because authxhr did not throw an error'); 129 | } catch (error) { 130 | expect(error.name).toEqual('RequestException'); 131 | expect(error.message).toEqual( 132 | JSON.stringify({ 133 | message: 'Mock cross-fetch error', 134 | status: 404, 135 | statusText: 'Not Found' 136 | }) 137 | ); 138 | expect(opts.body).toEqual(content); 139 | expect(opts.headers).toEqual(header); 140 | } 141 | }); 142 | 143 | it('works with OAuth', async () => { 144 | const expected = 'OK'; 145 | const res = await authxhr( 146 | { method: 'GET', path: 'https://whatev.er/file.json' }, 147 | { xhr: (opts, callback) => callback(null, expected) } 148 | ); 149 | expect(res).toBeTruthy(); 150 | expect(res).toEqual(expected); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /src/helpers/__tests__/xml.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { 4 | buildChangesetXml, 5 | convertElementXmlToJson, 6 | convertNotesXmlToJson, 7 | flattenAttributes, 8 | xmlToJson, 9 | jsonToXml, 10 | encodeXML, 11 | buildChangesetFromObjectXml, 12 | buildPreferencesFromObjectXml, 13 | cleanMapJson, 14 | convertElementsListXmlToJson, 15 | convertUserXmlToJson 16 | } from '../xml'; 17 | 18 | const nodeSample = fs.readFileSync( 19 | path.join(__dirname, '../../__mocks__/node.xml') 20 | ); 21 | const notesSample = fs.readFileSync( 22 | path.join(__dirname, '../../__mocks__/notes.xml') 23 | ); 24 | const wayFullSample = fs.readFileSync( 25 | path.join(__dirname, '../../__mocks__/way_full.xml') 26 | ); 27 | const userSample = fs.readFileSync( 28 | path.join(__dirname, '../../__mocks__/user.xml') 29 | ); 30 | 31 | jest.mock('../../../package.json', () => ({ 32 | version: '2.0.0' 33 | })); 34 | 35 | describe('XML helpers', () => { 36 | describe('buildChangesetXml', () => { 37 | it('Should build a stringified OSM changeset', () => { 38 | expect(buildChangesetXml('me', 'my comment')).toMatchSnapshot(); 39 | expect(buildChangesetXml()).toMatchSnapshot(); 40 | }); 41 | 42 | it('Should handle strings having double quotes', () => { 43 | expect( 44 | buildChangesetXml('My "app"', 'Doing some "weird" stuff') 45 | ).toMatchSnapshot(); 46 | }); 47 | 48 | it('Should handle optional tags', () => { 49 | expect( 50 | buildChangesetXml('My app', 'Doing some stuff', { 51 | key_example: 'value example' 52 | }) 53 | ).toMatchSnapshot(); 54 | }); 55 | }); 56 | 57 | describe('buildChangesetFromObjectXml', () => { 58 | it('Should build a stringified OSM changeset', () => { 59 | expect( 60 | buildChangesetFromObjectXml( 61 | { host: 'server.net', locale: 'fr' }, 62 | 'me', 63 | 'my comment' 64 | ) 65 | ).toMatchSnapshot(); 66 | expect(buildChangesetFromObjectXml()).toMatchSnapshot(); 67 | }); 68 | 69 | it('Should handle strings having double quotes', () => { 70 | expect( 71 | buildChangesetFromObjectXml( 72 | { host: 'server.net', locale: 'fr' }, 73 | 'My "app"', 74 | 'Doing some "weird" stuff' 75 | ) 76 | ).toMatchSnapshot(); 77 | }); 78 | }); 79 | 80 | describe('buildPreferencesFromObjectXml', () => { 81 | it('Should build stringified OSM preferences', () => { 82 | expect( 83 | buildPreferencesFromObjectXml({ locale: 'fr', color: 'red' }) 84 | ).toMatchSnapshot(); 85 | }); 86 | }); 87 | 88 | describe('convertElementXmlToJson', () => { 89 | it('Should convert an Element XML string into a proper JSON object', async () => { 90 | const result = await convertElementXmlToJson( 91 | nodeSample, 92 | 'node', 93 | '3683625932' 94 | ); 95 | expect(result).toMatchSnapshot(); 96 | }); 97 | }); 98 | 99 | describe('convertElementsListXmlToJson', () => { 100 | it('Should convert a list of Elements XML string into a proper JSON object', async () => { 101 | const result = await convertElementsListXmlToJson(wayFullSample, 'node'); 102 | expect(result).toMatchSnapshot(); 103 | }); 104 | }); 105 | 106 | describe('convertNotesXmlToJson', () => { 107 | it('Should convert a Notes XML string into a proper JSON object', async () => { 108 | const result = await convertNotesXmlToJson(notesSample); 109 | expect(result).toMatchSnapshot(); 110 | }); 111 | }); 112 | 113 | describe('convertUserXmlToJson', () => { 114 | it('Should convert an User XML string into a proper JSON object', async () => { 115 | const result = await convertUserXmlToJson(userSample); 116 | expect(result).toMatchSnapshot(); 117 | }); 118 | }); 119 | 120 | describe('flattenAttributes', () => { 121 | it('Should flatten the weird objects returned by xmlToJson', () => { 122 | const result = flattenAttributes({ 123 | $: { 124 | attribute1: 'value1', 125 | attribute2: 'value2' 126 | }, 127 | attribute3: ['value3'], 128 | attribute4: ['value4'], 129 | undefinedAttribute: undefined, 130 | emptylist: [] 131 | }); 132 | 133 | const expected = { 134 | attribute1: 'value1', 135 | attribute2: 'value2', 136 | attribute3: 'value3', 137 | attribute4: 'value4' 138 | }; 139 | expect(result).toEqual(expected); 140 | }); 141 | }); 142 | 143 | describe('xmlToJson', () => { 144 | it('Should convert an XML string into a JSON object', async () => { 145 | const result = await xmlToJson(notesSample); 146 | expect(result).toMatchSnapshot(); 147 | }); 148 | }); 149 | 150 | describe('jsonToXml', () => { 151 | it('Should convert a JSON object into an XML string', () => { 152 | const result = jsonToXml({ 153 | osm: { 154 | node: { 155 | $: { 156 | attribute1: 'value1', 157 | attribute2: 'value2' 158 | }, 159 | attribute3: ['value3'], 160 | attribute4: ['value4'] 161 | } 162 | } 163 | }); 164 | expect(result).toMatchSnapshot(); 165 | }); 166 | }); 167 | 168 | describe('encodeXML', () => { 169 | it('works', () => { 170 | const result = encodeXML("This is a parameter"); 171 | const expected = 172 | 'This is a <weird level='42'>parameter</weird>'; 173 | expect(result).toBe(expected); 174 | }); 175 | }); 176 | 177 | describe('cleanMapJson', () => { 178 | it('works', () => { 179 | const mapjson = { 180 | osm: { 181 | node: [ 182 | { 183 | $: { id: '1234', lat: '42.3', lon: '-1.3' }, 184 | tag: [ 185 | { 186 | $: { 187 | key: 'amenity', 188 | val: 'bicycle_parking' 189 | } 190 | } 191 | ] 192 | }, 193 | { 194 | $: { id: '1235', lat: '41.3', lon: '-1.2' } 195 | } 196 | ], 197 | way: [ 198 | { 199 | $: { id: '456' }, 200 | tag: [{ $: { highway: 'unclassified' } }], 201 | nd: [{ $: { ref: '1234' } }, { $: { ref: '1235' } }] 202 | } 203 | ], 204 | relation: [] 205 | } 206 | }; 207 | const result = cleanMapJson(mapjson); 208 | expect(result).toMatchSnapshot(); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 2 | [![GitHub Tag](https://img.shields.io/github/v/tag/osmlab/osm-request)](https://github.com/osmlab/osm-request/tags) 3 | [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/osmlab/osm-request/build-tests-coverage.yml)](https://github.com/osmlab/osm-request/actions/workflows/build-tests-coverage.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/osmlab/osm-request/badge.svg?branch=develop)](https://coveralls.io/github/osmlab/osm-request?branch=develop) 5 | 6 | # OSM Request 7 | 8 | Request the [OSM API](https://wiki.openstreetmap.org/wiki/API) (v0.6) from Javascript, with promises :) 9 | 10 | ## Installation 11 | 12 | ```sh 13 | npm install osm-request 14 | ``` 15 | 16 | ## Usage 17 | 18 | The full documentation of osm-request API is detailed in [the API documentation](API.md). 19 | 20 | OSM Request use the same configurations properties as [osm-auth 2.5.0](https://github.com/osmlab/osm-auth). So you can define your options in a constante conf variable and use it both for osm-request and osm-auth. 21 | 22 | ### Register your OAuth 2 application 23 | 24 | 1. Open https://www.openstreetmap.org/oauth2/applications 25 | 2. Click on `Register new application` 26 | 3. Choose a `name` for your app, example: `demo-app` 27 | 4. Choose a `Redirect URI` 28 | - For development, you can choose https://localhost and some port, it should be https. 29 | - For production, you can choose the url of your app, it should be https. 30 | 5. Choose the permission you need for your app. 31 | - read_prefs is the minimum 32 | - write_api is needed if your app can edit data 33 | 6. Consider choosing more permissions if needed 34 | - Read user preferences (read_prefs) 35 | - Modify user preferences (write_prefs) 36 | - Create diary entries, comments and make friends (write_diary) 37 | - Modify the map (write_api) 38 | - Read private GPS traces (read_gpx) 39 | - Upload GPS traces (write_gpx) 40 | - Modify notes (write_notes) 41 | - Redact map data (write_redactions) 42 | - Sign-in using OpenStreetMap (openid) 43 | 7. Click on `Register` 44 | 8. Make sure to copy the `Client ID` 45 | 9. The `Client Secret` is not needed, no need to copy it, also make sure not to disclose it 46 | 47 | Note that OAuth 2.0 do no need the `Client Secret` to work. So it is safe to publish the `Client ID` of your app online. For example, you can create a browser/JavaScript app containing your `Client ID` and publish it online (on GitHub pages, etc). 48 | 49 | ### Example with single page and auto login form 50 | 51 | The bellow example only show how to use osm-request. But to work, it needs first that you connect the user of your app to OSM throught osm-auth. To connect your user, please read osm-auth [readme](https://github.com/osmlab/osm-auth) 52 | 53 | ```javascript 54 | import OsmRequest from 'osm-request'; 55 | 56 | const conf = { 57 | scope: 'read_prefs write_api', // To customize 58 | client_id: "YOUR_CLIENT_ID", // To customize 59 | redirect_uri: '', 60 | url: 'https://www.openstreetmap.org', 61 | apiUrl: 'https://api.openstreetmap.org', 62 | auto: true, 63 | singlepage: true 64 | }; 65 | 66 | const osmRequest = new OsmRequest(conf); 67 | 68 | async function start() { 69 | let element = await osmRequest.fetchElement('node/3683625932'); 70 | element = osmRequest.setTag(element, 'key', 'value'); 71 | element = osmRequest.setTags(element, { 72 | key1: 'value1', 73 | key2: 'value2', 74 | key3: 'value3', 75 | }); 76 | element = osmRequest.removeTag(element, 'key2'); 77 | element = osmRequest.setTimestampToNow(element); 78 | element = osmRequest.setCoordinates(element, 1.234, 0.456); 79 | 80 | const changesetId = await osmRequest.createChangeset('Created by me', 'My changeset comment'); 81 | const isChangesetStillOpen = await osmRequest.isChangesetStillOpen(changesetId); 82 | const newElementVersion = await osmRequest.sendElement(element, changesetId); 83 | element = osmRequest.setVersion(element, newElementVersion); 84 | } 85 | 86 | start(); 87 | ``` 88 | 89 | For the OSM dev instance, use that apiUrl: https://api06.dev.openstreetmap.org 90 | 91 | ## Contribute 92 | 93 | To start contribute on this project, you can retrieve code using the following commands: 94 | 95 | ```sh 96 | git clone git@github.com:osmlab/osm-request.git 97 | cd osm-request 98 | npm install 99 | npm run watch 100 | ``` 101 | 102 | This project uses a specific work flow for branches: 103 | 104 | * `master` branch is dedicated to releases, managed by repo maintainers 105 | * `develop` branch is for currently developed version, managed by repo maintainers 106 | * `feature/...` branches are for all developers, working on a particular feature 107 | 108 | Pull requests are welcome, as the project is fully open-source. If you want to work on new features, please create a branch named `feature/yourFeatureName`. When work is done, open a pull request to merge your branch on `develop` branch. The code will be reviewed by one or several developers before being merged, in order to keep a good code quality. 109 | 110 | ### eslint version 8.X 111 | 112 | For now eslint stays in version 8.X. We need to wait until babel, babel plugins and eslint plugins (that we use), are compatibles with version 9.X of eslint. 113 | 114 | ## Make a release 115 | 116 | ```sh 117 | git checkout develop 118 | git pull origin develop 119 | npm version patch -m "release: %s" 120 | npm publish 121 | git checkout master 122 | git pull origin master 123 | git merge develop 124 | git push origin master 125 | ``` 126 | 127 | `npm version` tests the code, builds it and updates the doc. Then it upgrades the package version number according to the used keyword (patch, minor or major) and commits the modifications in Git (with a proper version tag). Finally, it pushes it to repository with the tag. 128 | 129 | ## Apps or libs using osm-request 130 | 131 | - [Balluchon](https://gitlab.limos.fr/iia_braikeh/balluchon) 132 | - [BANCO](https://github.com/PanierAvide/EditeurCommercesOSMFR) 133 | - [Bike Ottawa Interactive Maps](https://github.com/BikeOttawa/maps.bikeottawa.ca-frontend) 134 | - [Busy-Hours](https://github.com/Jungle-Bus/Busy-Hours) 135 | - [Ça reste ouvert](https://github.com/caresteouvert/caresteouvert_backend) 136 | - [OSM Mobile tag it](https://github.com/toutpt/osm-mobile-tagit) 137 | - [osm-edit-bundle](https://www.npmjs.com/package/osm-edit-bundle) 138 | - [OsmInEdit](https://framagit.org/PanierAvide/osminedit) 139 | - [Parking Mapper](https://github.com/Binnette/parking-mapper) 140 | - [Pic4Review](https://framagit.org/Pic4Carto/Pic4Review) 141 | - [POIEditor](https://github.com/francois2metz/poieditor) 142 | - [ProjetDuMois](https://github.com/vdct/ProjetDuMois) 143 | - [tumulus](https://github.com/superrache/tumulus) 144 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | /** @type {import('jest').Config} */ 7 | const config = { 8 | // All imported modules in your tests should be mocked automatically 9 | // automock: false, 10 | 11 | // Stop running tests after `n` failures 12 | // bail: 0, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/tmp/jest_rs", 16 | 17 | // Automatically clear mock calls, instances, contexts and results before every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | collectCoverageFrom: [ 25 | "src/**/*.js", 26 | "!/node_modules/" 27 | ], 28 | 29 | // The directory where Jest should output its coverage files 30 | coverageDirectory: "coverage", 31 | 32 | // An array of regexp pattern strings used to skip coverage collection 33 | coveragePathIgnorePatterns: [ 34 | "/src/requests.js", 35 | "/src/helpers/time.js", 36 | "/node_modules/" 37 | ], 38 | 39 | // Indicates which provider should be used to instrument code for coverage 40 | // coverageProvider: "babel", 41 | 42 | // A list of reporter names that Jest uses when writing coverage reports 43 | coverageReporters: [ 44 | "json", 45 | "lcov", 46 | ], 47 | 48 | // An object that configures minimum threshold enforcement for coverage results 49 | // coverageThreshold: undefined, 50 | 51 | // A path to a custom dependency extractor 52 | // dependencyExtractor: undefined, 53 | 54 | // Make calling deprecated APIs throw helpful error messages 55 | // errorOnDeprecated: false, 56 | 57 | // The default configuration for fake timers 58 | // fakeTimers: { 59 | // "enableGlobally": false 60 | // }, 61 | 62 | // Force coverage collection from ignored files using an array of glob patterns 63 | // forceCoverageMatch: [], 64 | 65 | // A path to a module which exports an async function that is triggered once before all test suites 66 | // globalSetup: undefined, 67 | 68 | // A path to a module which exports an async function that is triggered once after all test suites 69 | // globalTeardown: undefined, 70 | 71 | // A set of global variables that need to be available in all test environments 72 | // globals: {}, 73 | 74 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 75 | // maxWorkers: "50%", 76 | 77 | // An array of directory names to be searched recursively up from the requiring module's location 78 | // moduleDirectories: [ 79 | // "node_modules" 80 | // ], 81 | 82 | // An array of file extensions your modules use 83 | // moduleFileExtensions: [ 84 | // "js", 85 | // "mjs", 86 | // "cjs", 87 | // "jsx", 88 | // "ts", 89 | // "tsx", 90 | // "json", 91 | // "node" 92 | // ], 93 | 94 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 95 | // moduleNameMapper: {}, 96 | 97 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 98 | // modulePathIgnorePatterns: [], 99 | 100 | // Activates notifications for test results 101 | // notify: false, 102 | 103 | // An enum that specifies notification mode. Requires { notify: true } 104 | // notifyMode: "failure-change", 105 | 106 | // A preset that is used as a base for Jest's configuration 107 | // preset: undefined, 108 | 109 | // Run tests from one or more projects 110 | // projects: undefined, 111 | 112 | // Use this configuration option to add custom reporters to Jest 113 | // reporters: undefined, 114 | 115 | // Automatically reset mock state before every test 116 | // resetMocks: false, 117 | 118 | // Reset the module registry before running each individual test 119 | // resetModules: false, 120 | 121 | // A path to a custom resolver 122 | // resolver: undefined, 123 | 124 | // Automatically restore mock state and implementation before every test 125 | // restoreMocks: false, 126 | 127 | // The root directory that Jest should scan for tests and modules within 128 | // rootDir: undefined, 129 | 130 | // A list of paths to directories that Jest should use to search for files in 131 | // roots: [ 132 | // "" 133 | // ], 134 | 135 | // Allows you to use a custom runner instead of Jest's default test runner 136 | // runner: "jest-runner", 137 | 138 | // The paths to modules that run some code to configure or set up the testing environment before each test 139 | // setupFiles: [], 140 | 141 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 142 | // setupFilesAfterEnv: [], 143 | 144 | // The number of seconds after which a test is considered as slow and reported as such in the results. 145 | // slowTestThreshold: 5, 146 | 147 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 148 | // snapshotSerializers: [], 149 | 150 | snapshotFormat: { 151 | "escapeString": true, 152 | "printBasicPrototype": true 153 | }, 154 | 155 | // The test environment that will be used for testing 156 | testEnvironment: "jsdom", 157 | 158 | // Options that will be passed to the testEnvironment 159 | // testEnvironmentOptions: {}, 160 | 161 | // Adds a location field to test results 162 | // testLocationInResults: false, 163 | 164 | // The glob patterns Jest uses to detect test files 165 | // testMatch: [ 166 | // "**/__tests__/**/*.[jt]s?(x)", 167 | // "**/?(*.)+(spec|test).[tj]s?(x)" 168 | // ], 169 | 170 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 171 | // testPathIgnorePatterns: [ 172 | // "/node_modules/" 173 | // ], 174 | 175 | // The regexp pattern or array of patterns that Jest uses to detect test files 176 | // testRegex: [], 177 | 178 | // This option allows the use of a custom results processor 179 | // testResultsProcessor: undefined, 180 | 181 | // This option allows use of a custom test runner 182 | // testRunner: "jest-circus/runner", 183 | 184 | // A map from regular expressions to paths to transformers 185 | // transform: undefined, 186 | 187 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 188 | // transformIgnorePatterns: [ 189 | // "/node_modules/", 190 | // "\\.pnp\\.[^\\/]+$" 191 | // ], 192 | 193 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 194 | // unmockedModulePathPatterns: undefined, 195 | 196 | // Indicates whether each individual test should be reported during the run 197 | verbose: true, 198 | 199 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 200 | // watchPathIgnorePatterns: [], 201 | 202 | // Whether to use watchman for file crawling 203 | // watchman: true, 204 | }; 205 | 206 | module.exports = config; 207 | -------------------------------------------------------------------------------- /src/helpers/xml.js: -------------------------------------------------------------------------------- 1 | import { parseString, Builder } from 'xml2js'; 2 | import packageJson from '../../package.json'; 3 | 4 | const osmRequestVersion = packageJson.version; 5 | 6 | /** 7 | * Escape a string to make it XML parameter-safe 8 | * @param {string} str 9 | * @return {string} 10 | */ 11 | export function encodeXML(str = '') { 12 | return str 13 | .replace(/&/g, '&') 14 | .replace(//g, '>') 16 | .replace(/"/g, '"') 17 | .replace(/'/g, '''); 18 | } 19 | 20 | /** 21 | * Build a stringified OSM changeset 22 | * @param {string} [createdBy] 23 | * @param {string} [comment] 24 | * @param {string} [optionalTags] Keys values to set tags 25 | * @return {string} 26 | */ 27 | export function buildChangesetXml( 28 | createdBy = '', 29 | comment = '', 30 | optionalTags = {} 31 | ) { 32 | const tags = Object.entries(optionalTags).map(entry => { 33 | return ``; 34 | }); 35 | return ` 36 | 37 | 38 | 39 | 40 | 41 | ${tags.join(`\n `)} 42 | 43 | 44 | `; 45 | } 46 | 47 | /** 48 | * Build an OSM changeset from keys values, intended for update 49 | * @param {Object} tags To set tags 50 | * @param {string} [createdBy] 51 | * @param {string} [comment] 52 | * @return {string} 53 | */ 54 | export function buildChangesetFromObjectXml( 55 | tags = {}, 56 | createdBy = '', 57 | comment = '' 58 | ) { 59 | const tagsArray = Object.entries(tags).map(entry => { 60 | return ``; 61 | }); 62 | return ` 63 | 64 | 65 | 66 | 67 | 68 | ${tagsArray.join(`\n `)} 69 | 70 | 71 | `; 72 | } 73 | 74 | /** 75 | * Build an OSM preferences XML from object keys values 76 | * @param {Object} prefs The preferences values 77 | * @return {string} 78 | */ 79 | export function buildPreferencesFromObjectXml(prefs) { 80 | const preferences = Object.entries(prefs).map(entry => { 81 | return ``; 82 | }); 83 | return ` 84 | 85 | 86 | ${preferences.join(`\n `)} 87 | 88 | 89 | `; 90 | } 91 | 92 | /** 93 | * Convert a raw Element API response into a well formatted JSON object 94 | * @param {string} xml - The raw API response 95 | * @param {string} elementType - The type of the concerned OSM element (eg: node, way, relation) 96 | * @param {string} elementId - The ID of the concerned OSM element 97 | * @return {Promise} 98 | */ 99 | export function convertElementXmlToJson(xml, elementType, elementId) { 100 | return xmlToJson(xml).then(result => { 101 | const element = result.osm[elementType][0]; 102 | element._id = elementId; 103 | element._type = elementType; 104 | return element; 105 | }); 106 | } 107 | 108 | /** 109 | * Convert a JSON object with OSM map features into a well formatted JSON object 110 | * @param {Object} osmMapJson - The raw API response 111 | * @return {Array} 112 | */ 113 | export function cleanMapJson(osmMapJson) { 114 | const { bounds } = osmMapJson.osm; 115 | const mapper = type => e => ({ ...e, _id: e['$'].id, _type: type }); 116 | 117 | let way = []; 118 | if (osmMapJson.osm.way) { 119 | way = osmMapJson.osm.way.map(mapper('way')); 120 | } 121 | let node = []; 122 | if (osmMapJson.osm.node) { 123 | node = osmMapJson.osm.node.map(mapper('node')); 124 | } 125 | let relation = []; 126 | if (osmMapJson.osm.relation) { 127 | relation = osmMapJson.osm.relation.map(mapper('relation')); 128 | } 129 | 130 | const newOsmObject = {}; 131 | Object.entries({ node: node, way: way, relation: relation }).map(entry => { 132 | if (entry[0] in osmMapJson.osm) { 133 | newOsmObject[entry[0]] = entry[1]; 134 | } 135 | }); 136 | 137 | if (bounds) { 138 | newOsmObject.bounds = bounds; 139 | } 140 | return newOsmObject; 141 | } 142 | 143 | /** 144 | * Convert a raw list of elements API response into a well formatted JSON object 145 | * @param {string} xml - The raw API response 146 | * @param {string} type The OSM element type (node, way, relation) 147 | * @return {Promise} 148 | */ 149 | export function convertElementsListXmlToJson(xml, type) { 150 | return xmlToJson(xml).then(result => { 151 | return result.osm[type] 152 | ? result.osm[type].map(e => { 153 | e._id = e.$.id; 154 | e._type = type; 155 | return e; 156 | }) 157 | : []; 158 | }); 159 | } 160 | 161 | /** 162 | * Convert a raw Notes API response into a well formatted JSON object 163 | * @param {string} xml - The raw API response 164 | * @return {Promise} 165 | */ 166 | export function convertNotesXmlToJson(xml) { 167 | return xmlToJson(xml) 168 | .then(result => { 169 | if (result.osm.note) { 170 | return result.osm.note; 171 | } else { 172 | return []; 173 | } 174 | }) 175 | .then(notes => 176 | notes.map(note => { 177 | const returnedNote = flattenAttributes(note); 178 | 179 | returnedNote.comments = [ 180 | ...returnedNote.comments.comment.map(comment => 181 | flattenAttributes(comment) 182 | ) 183 | ]; 184 | 185 | return returnedNote; 186 | }) 187 | ); 188 | } 189 | 190 | /** 191 | * Convert a raw User API response into a well formatted JSON object 192 | * @param {string} xml - The raw API response 193 | * @return {Promise} 194 | */ 195 | export function convertUserXmlToJson(xml) { 196 | return xmlToJson(xml).then(result => { 197 | const user = flattenAttributes(result.osm.user[0]); 198 | Object.entries(user) 199 | .filter(e => e[1].$) 200 | .forEach(e => { 201 | user[e[0]] = flattenAttributes(e[1]); 202 | }); 203 | if (user.blocks.received) { 204 | user.blocks.received = user.blocks.received.map(b => 205 | flattenAttributes(b) 206 | ); 207 | } 208 | return user; 209 | }); 210 | } 211 | 212 | /** 213 | * XML converted with the xmlToJson function contain some weird objects 214 | * Eg: 215 | * { 216 | * $: { 217 | * attribute1: 'value1', 218 | * attribute2: 'value2' 219 | * }, 220 | * attribute3: ['value3'], 221 | * attribute4: ['value4'], 222 | * } 223 | * 224 | * That function flatten them 225 | * Eg: 226 | * { 227 | * attribute1: 'value1', 228 | * attribute2: 'value2', 229 | * attribute3: 'value3', 230 | * attribute4: 'value4', 231 | * } 232 | * 233 | * @param {Object} object 234 | * @return {Object} 235 | */ 236 | export function flattenAttributes(object) { 237 | const flatObject = { 238 | ...object.$ 239 | }; 240 | 241 | Object.keys(object).forEach(key => { 242 | if (key === '$') return; 243 | if (!object[key]) return; 244 | if (object[key].length === 0) return; 245 | 246 | flatObject[key] = object[key][0]; 247 | }); 248 | 249 | return flatObject; 250 | } 251 | 252 | /** 253 | * Convert a stringified XML into a JSON object 254 | * @param {string} xml 255 | * @return {Promise} 256 | */ 257 | export function xmlToJson(xml) { 258 | return new Promise((resolve, reject) => { 259 | parseString(xml, (err, result) => { 260 | if (err) reject(err); 261 | 262 | resolve(result); 263 | }); 264 | }); 265 | } 266 | 267 | /** 268 | * Convert a JSON object into a stringified XML 269 | * @param {Object} json 270 | * @return {string} 271 | */ 272 | export function jsonToXml(json) { 273 | const builder = new Builder({ 274 | xmldec: { 275 | version: '1.0', 276 | encoding: 'UTF-8', 277 | standalone: null 278 | } 279 | }); 280 | return builder.buildObject(json); 281 | } 282 | -------------------------------------------------------------------------------- /src/helpers/__tests__/__snapshots__/xml.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`XML helpers buildChangesetFromObjectXml Should build a stringified OSM changeset 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | " 15 | `; 16 | 17 | exports[`XML helpers buildChangesetFromObjectXml Should build a stringified OSM changeset 2`] = ` 18 | " 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | " 28 | `; 29 | 30 | exports[`XML helpers buildChangesetFromObjectXml Should handle strings having double quotes 1`] = ` 31 | " 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | " 42 | `; 43 | 44 | exports[`XML helpers buildChangesetXml Should build a stringified OSM changeset 1`] = ` 45 | " 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | " 55 | `; 56 | 57 | exports[`XML helpers buildChangesetXml Should build a stringified OSM changeset 2`] = ` 58 | " 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | " 68 | `; 69 | 70 | exports[`XML helpers buildChangesetXml Should handle strings having double quotes 1`] = ` 71 | " 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | " 81 | `; 82 | 83 | exports[`XML helpers buildChangesetXml Should handle optional tags 1`] = ` 84 | " 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | " 94 | `; 95 | 96 | exports[`XML helpers buildPreferencesFromObjectXml Should build stringified OSM preferences 1`] = ` 97 | " 98 | 99 | 100 | 101 | 102 | 103 | 104 | " 105 | `; 106 | 107 | exports[`XML helpers cleanMapJson works 1`] = ` 108 | Object { 109 | "node": Array [ 110 | Object { 111 | "$": Object { 112 | "id": "1234", 113 | "lat": "42.3", 114 | "lon": "-1.3", 115 | }, 116 | "_id": "1234", 117 | "_type": "node", 118 | "tag": Array [ 119 | Object { 120 | "$": Object { 121 | "key": "amenity", 122 | "val": "bicycle_parking", 123 | }, 124 | }, 125 | ], 126 | }, 127 | Object { 128 | "$": Object { 129 | "id": "1235", 130 | "lat": "41.3", 131 | "lon": "-1.2", 132 | }, 133 | "_id": "1235", 134 | "_type": "node", 135 | }, 136 | ], 137 | "relation": Array [], 138 | "way": Array [ 139 | Object { 140 | "$": Object { 141 | "id": "456", 142 | }, 143 | "_id": "456", 144 | "_type": "way", 145 | "nd": Array [ 146 | Object { 147 | "$": Object { 148 | "ref": "1234", 149 | }, 150 | }, 151 | Object { 152 | "$": Object { 153 | "ref": "1235", 154 | }, 155 | }, 156 | ], 157 | "tag": Array [ 158 | Object { 159 | "$": Object { 160 | "highway": "unclassified", 161 | }, 162 | }, 163 | ], 164 | }, 165 | ], 166 | } 167 | `; 168 | 169 | exports[`XML helpers convertElementXmlToJson Should convert an Element XML string into a proper JSON object 1`] = ` 170 | Object { 171 | "$": Object { 172 | "changeset": "33150668", 173 | "id": "3683625932", 174 | "lat": "44.8331455", 175 | "lon": "-0.5936602", 176 | "timestamp": "2015-08-06T09:49:47Z", 177 | "uid": "2568974", 178 | "user": "Vinber-Num&Lib", 179 | "version": "1", 180 | "visible": "true", 181 | }, 182 | "_id": "3683625932", 183 | "_type": "node", 184 | "tag": Array [ 185 | Object { 186 | "$": Object { 187 | "k": "amenity", 188 | "v": "drinking_water", 189 | }, 190 | }, 191 | ], 192 | } 193 | `; 194 | 195 | exports[`XML helpers convertElementsListXmlToJson Should convert a list of Elements XML string into a proper JSON object 1`] = ` 196 | Array [ 197 | Object { 198 | "$": Object { 199 | "changeset": "60117678", 200 | "id": "1531122358", 201 | "lat": "48.2210040", 202 | "lon": "-1.7884326", 203 | "timestamp": "2018-06-24T10:47:29Z", 204 | "uid": "214436", 205 | "user": "PanierAvide", 206 | "version": "2", 207 | "visible": "true", 208 | }, 209 | "_id": "1531122358", 210 | "_type": "node", 211 | }, 212 | Object { 213 | "$": Object { 214 | "changeset": "60117678", 215 | "id": "1531122360", 216 | "lat": "48.2210155", 217 | "lon": "-1.7883932", 218 | "timestamp": "2018-06-24T10:47:29Z", 219 | "uid": "214436", 220 | "user": "PanierAvide", 221 | "version": "2", 222 | "visible": "true", 223 | }, 224 | "_id": "1531122360", 225 | "_type": "node", 226 | }, 227 | Object { 228 | "$": Object { 229 | "changeset": "60117678", 230 | "id": "1531122361", 231 | "lat": "48.2210386", 232 | "lon": "-1.7883679", 233 | "timestamp": "2018-06-24T10:47:29Z", 234 | "uid": "214436", 235 | "user": "PanierAvide", 236 | "version": "2", 237 | "visible": "true", 238 | }, 239 | "_id": "1531122361", 240 | "_type": "node", 241 | }, 242 | Object { 243 | "$": Object { 244 | "changeset": "60117678", 245 | "id": "1531122362", 246 | "lat": "48.2210245", 247 | "lon": "-1.7885098", 248 | "timestamp": "2018-06-24T10:47:29Z", 249 | "uid": "214436", 250 | "user": "PanierAvide", 251 | "version": "2", 252 | "visible": "true", 253 | }, 254 | "_id": "1531122362", 255 | "_type": "node", 256 | }, 257 | Object { 258 | "$": Object { 259 | "changeset": "60117678", 260 | "id": "1531122366", 261 | "lat": "48.2210793", 262 | "lon": "-1.7885211", 263 | "timestamp": "2018-06-24T10:47:29Z", 264 | "uid": "214436", 265 | "user": "PanierAvide", 266 | "version": "2", 267 | "visible": "true", 268 | }, 269 | "_id": "1531122366", 270 | "_type": "node", 271 | }, 272 | Object { 273 | "$": Object { 274 | "changeset": "60117678", 275 | "id": "1531122369", 276 | "lat": "48.2210840", 277 | "lon": "-1.7883719", 278 | "timestamp": "2018-06-24T10:47:29Z", 279 | "uid": "214436", 280 | "user": "PanierAvide", 281 | "version": "2", 282 | "visible": "true", 283 | }, 284 | "_id": "1531122369", 285 | "_type": "node", 286 | }, 287 | Object { 288 | "$": Object { 289 | "changeset": "60117678", 290 | "id": "1531122371", 291 | "lat": "48.2211072", 292 | "lon": "-1.7884825", 293 | "timestamp": "2018-06-24T10:47:29Z", 294 | "uid": "214436", 295 | "user": "PanierAvide", 296 | "version": "2", 297 | "visible": "true", 298 | }, 299 | "_id": "1531122371", 300 | "_type": "node", 301 | }, 302 | Object { 303 | "$": Object { 304 | "changeset": "60117678", 305 | "id": "1531122372", 306 | "lat": "48.2211118", 307 | "lon": "-1.7884258", 308 | "timestamp": "2018-06-24T10:47:29Z", 309 | "uid": "214436", 310 | "user": "PanierAvide", 311 | "version": "2", 312 | "visible": "true", 313 | }, 314 | "_id": "1531122372", 315 | "_type": "node", 316 | }, 317 | Object { 318 | "$": Object { 319 | "changeset": "60117678", 320 | "id": "5715015332", 321 | "lat": "48.2210073", 322 | "lon": "-1.7884753", 323 | "timestamp": "2018-06-24T10:47:23Z", 324 | "uid": "214436", 325 | "user": "PanierAvide", 326 | "version": "1", 327 | "visible": "true", 328 | }, 329 | "_id": "5715015332", 330 | "_type": "node", 331 | }, 332 | Object { 333 | "$": Object { 334 | "changeset": "60117678", 335 | "id": "5715015333", 336 | "lat": "48.2210508", 337 | "lon": "-1.7885265", 338 | "timestamp": "2018-06-24T10:47:23Z", 339 | "uid": "214436", 340 | "user": "PanierAvide", 341 | "version": "1", 342 | "visible": "true", 343 | }, 344 | "_id": "5715015333", 345 | "_type": "node", 346 | }, 347 | Object { 348 | "$": Object { 349 | "changeset": "60117678", 350 | "id": "5715015334", 351 | "lat": "48.2210955", 352 | "lon": "-1.7885055", 353 | "timestamp": "2018-06-24T10:47:23Z", 354 | "uid": "214436", 355 | "user": "PanierAvide", 356 | "version": "1", 357 | "visible": "true", 358 | }, 359 | "_id": "5715015334", 360 | "_type": "node", 361 | }, 362 | Object { 363 | "$": Object { 364 | "changeset": "60117678", 365 | "id": "5715015335", 366 | "lat": "48.2211129", 367 | "lon": "-1.7884548", 368 | "timestamp": "2018-06-24T10:47:23Z", 369 | "uid": "214436", 370 | "user": "PanierAvide", 371 | "version": "1", 372 | "visible": "true", 373 | }, 374 | "_id": "5715015335", 375 | "_type": "node", 376 | }, 377 | Object { 378 | "$": Object { 379 | "changeset": "60117678", 380 | "id": "5715015336", 381 | "lat": "48.2210616", 382 | "lon": "-1.7883625", 383 | "timestamp": "2018-06-24T10:47:23Z", 384 | "uid": "214436", 385 | "user": "PanierAvide", 386 | "version": "1", 387 | "visible": "true", 388 | }, 389 | "_id": "5715015336", 390 | "_type": "node", 391 | }, 392 | Object { 393 | "$": Object { 394 | "changeset": "60117678", 395 | "id": "5715015337", 396 | "lat": "48.2211018", 397 | "lon": "-1.7883943", 398 | "timestamp": "2018-06-24T10:47:23Z", 399 | "uid": "214436", 400 | "user": "PanierAvide", 401 | "version": "1", 402 | "visible": "true", 403 | }, 404 | "_id": "5715015337", 405 | "_type": "node", 406 | }, 407 | ] 408 | `; 409 | 410 | exports[`XML helpers convertNotesXmlToJson Should convert a Notes XML string into a proper JSON object 1`] = ` 411 | Array [ 412 | Object { 413 | "close_url": "https://api.openstreetmap.org/api/0.6/notes/1369237/close", 414 | "comment_url": "https://api.openstreetmap.org/api/0.6/notes/1369237/comment", 415 | "comments": Array [ 416 | Object { 417 | "action": "opened", 418 | "date": "2018-04-22 17:54:49 UTC", 419 | "html": "

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 | 560 | 561 | value3 562 | value4 563 | 564 | " 565 | `; 566 | 567 | exports[`XML helpers xmlToJson Should convert an XML string into a JSON object 1`] = ` 568 | Object { 569 | "osm": Object { 570 | "$": Object { 571 | "generator": "OpenStreetMap server", 572 | "version": "0.6", 573 | }, 574 | "note": Array [ 575 | Object { 576 | "$": Object { 577 | "lat": "44.4068500", 578 | "lon": "0.5891000", 579 | }, 580 | "close_url": Array [ 581 | "https://api.openstreetmap.org/api/0.6/notes/1369237/close", 582 | ], 583 | "comment_url": Array [ 584 | "https://api.openstreetmap.org/api/0.6/notes/1369237/comment", 585 | ], 586 | "comments": Array [ 587 | Object { 588 | "comment": Array [ 589 | Object { 590 | "action": Array [ 591 | "opened", 592 | ], 593 | "date": Array [ 594 | "2018-04-22 17:54:49 UTC", 595 | ], 596 | "html": Array [ 597 | "

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} nodeOsmIds 368 | * @param {Object} [tags] Optional, initial tags 369 | * @param {string} [id] Optional, identifier for OSM element 370 | * @return {Object} 371 | */ 372 | createWayElement(nodeOsmIds, tags = {}, id) { 373 | const way = { 374 | $: {}, 375 | nd: nodeOsmIds.map(id => ({ 376 | $: { 377 | ref: findElementId(id) 378 | } 379 | })), 380 | tag: [], 381 | _type: 'way' 382 | }; 383 | 384 | if (typeof id !== 'undefined') { 385 | way._id = id.toString(); 386 | } 387 | 388 | way.tag = Object.keys(tags).map(tagName => ({ 389 | $: { 390 | k: tagName.toString(), 391 | v: tags[tagName].toString() 392 | } 393 | })); 394 | return way; 395 | } 396 | 397 | /** 398 | * Create a shiny new OSM relation element, in a JSON format 399 | * @param {Array} osmElements Array of object with keys id and optional role key. Key id contains an osmId value like 'node/1234' 400 | * @param {Object} [tags] Optional, initial tags 401 | * @param {string} [id] Optional, identifier for OSM element 402 | * @return {Object} 403 | */ 404 | createRelationElement(osmElements, tags = {}, id) { 405 | const relation = { 406 | $: {}, 407 | member: osmElements.map(elementObject => { 408 | const { id, role } = elementObject; 409 | const elementType = findElementType(id); 410 | const elementId = findElementId(id); 411 | const elementObjectCopy = { 412 | type: elementType, 413 | ref: elementId 414 | }; 415 | if (typeof role !== 'undefined') { 416 | elementObjectCopy.role = elementObject.role; 417 | } 418 | return { 419 | $: elementObjectCopy 420 | }; 421 | }), 422 | tag: [], 423 | _type: 'relation' 424 | }; 425 | 426 | if (typeof id !== 'undefined') { 427 | relation._id = id.toString(); 428 | } 429 | 430 | relation.tag = Object.keys(tags).map(tagName => ({ 431 | $: { 432 | k: tagName.toString(), 433 | v: tags[tagName].toString() 434 | } 435 | })); 436 | return relation; 437 | } 438 | 439 | /** 440 | * Fetch an OSM element by its ID and optionnally 441 | * all other elements referenced by it 442 | * @param {string} osmId Eg: node/12345 443 | * @param {Object} options Optional parameters 444 | * @param {boolean} [options.full] True for getting all elements referenced by this element 445 | * @return {Promise} 446 | */ 447 | fetchElement(osmId, options) { 448 | if (options && options.full) { 449 | return fetchElementRequestFull( 450 | this.apiUrl, 451 | osmId, 452 | this._options.always_authenticated ? { auth: this._auth } : {} 453 | ); 454 | } else { 455 | return fetchElementRequest( 456 | this.apiUrl, 457 | osmId, 458 | this._options.always_authenticated ? { auth: this._auth } : {} 459 | ); 460 | } 461 | } 462 | 463 | /** 464 | * Fetch multiple OSM elements by it full OSM IDs. Work only with a type of elements, no mixed elements type are allowed 465 | * @param {Array} osmIds Eg: ['node/12345', 'node/6789']. We do not support optional version e.g 'node/12345v2' 466 | * @return {Promise} 467 | */ 468 | fetchMultipleElements(osmIds) { 469 | return multiFetchElementsByTypeRequest( 470 | this.apiUrl, 471 | osmIds, 472 | this._options.always_authenticated ? { auth: this._auth } : {} 473 | ); 474 | } 475 | 476 | /** 477 | * Fetch relation(s) from an OSM element 478 | * @param {string} osmId Eg: node/12345 479 | * @return {Promise} 480 | */ 481 | fetchRelationsForElement(osmId) { 482 | return fetchRelationsForElementRequest( 483 | this.apiUrl, 484 | osmId, 485 | this._options.always_authenticated ? { auth: this._auth } : {} 486 | ); 487 | } 488 | 489 | /** 490 | * Fetch ways using the given OSM node 491 | * @param {string} osmId Eg: node/12345 492 | * @return {Promise} Resolve on ways array (each one can be used as an Element for all other functions) 493 | */ 494 | fetchWaysForNode(osmId) { 495 | return fetchWaysForNodeRequest( 496 | this.apiUrl, 497 | osmId, 498 | this._options.always_authenticated ? { auth: this._auth } : {} 499 | ); 500 | } 501 | 502 | /** 503 | * Find an element with it OsmId within an OSM collection 504 | * @param {Object} json An object with key that can be 'node', 'way', 'relation' 505 | * @param {string} osmId Eg: node/12345 506 | * @return {Obejct} OSM element 507 | */ 508 | findElementWithinOSMCollection(json, osmId) { 509 | const elementType = findElementType(osmId); 510 | const elementId = findElementId(osmId); 511 | if (elementType in json) { 512 | return json[elementType].find(element => element.$.id === elementId); 513 | } 514 | return undefined; 515 | } 516 | 517 | /** 518 | * Get all tags of this element as an object 519 | * @param {Object} element 520 | * @return {Object} Tags of this element 521 | */ 522 | getTags(element) { 523 | const tags = {}; 524 | if (element && element.tag) { 525 | element.tag.forEach(t => { 526 | tags[t.$.k] = t.$.v; 527 | }); 528 | } 529 | return tags; 530 | } 531 | 532 | /** 533 | * Use setTag instead. Will be removed in the future. 534 | * @deprecated 535 | */ 536 | setProperty(element, propertyName, propertyValue) { 537 | return this.setTag(element, propertyName, propertyValue); 538 | } 539 | 540 | /** 541 | * Add or replace a tag in a given element 542 | * @param {Object} element 543 | * @param {string} tagName 544 | * @param {string} tagValue 545 | * @return {Object} A new version of the element 546 | */ 547 | setTag(element, tagName, tagValue) { 548 | const newElement = simpleObjectDeepClone(element); 549 | const filteredTags = newElement.tag 550 | ? newElement.tag.filter(tag => tag.$.k !== tagName.toString()) 551 | : []; 552 | 553 | newElement.tag = [ 554 | ...filteredTags, 555 | { 556 | $: { 557 | k: tagName.toString(), 558 | v: tagValue.toString() 559 | } 560 | } 561 | ]; 562 | 563 | return newElement; 564 | } 565 | 566 | /** 567 | * Use setTags instead. Will be removed in the future. 568 | * @deprecated 569 | */ 570 | setProperties(element, properties) { 571 | return this.setTags(element, properties); 572 | } 573 | 574 | /** 575 | * Add or replace several tags in a given element 576 | * @param {Object} element 577 | * @param {Object} tags 578 | * @return {Object} A new version of the element 579 | */ 580 | setTags(element, tags) { 581 | const newElement = simpleObjectDeepClone(element); 582 | const clonedTags = simpleObjectDeepClone(tags); 583 | const tagsName = Object.keys(clonedTags); 584 | 585 | const filteredTags = newElement.tag 586 | ? newElement.tag.filter(tag => !tagsName.includes(tag.$.k)) 587 | : []; 588 | const formattedTags = tagsName.map(tagName => ({ 589 | $: { 590 | k: tagName.toString(), 591 | v: clonedTags[tagName].toString() 592 | } 593 | })); 594 | 595 | newElement.tag = [...filteredTags, ...formattedTags]; 596 | 597 | return newElement; 598 | } 599 | 600 | /** 601 | * Completely replace tags of a given element 602 | * @param {Object} element 603 | * @param {Object} tags The tags that will replace completely current element tags 604 | * @return {Object} A new version of the element 605 | */ 606 | replaceTags(element, tags) { 607 | const newElement = simpleObjectDeepClone(element); 608 | const clonedTags = simpleObjectDeepClone(tags); 609 | const formattedTags = Object.entries(clonedTags).map(kv => ({ 610 | $: { 611 | k: kv[0].toString(), 612 | v: kv[1].toString() 613 | } 614 | })); 615 | 616 | newElement.tag = formattedTags; 617 | return newElement; 618 | } 619 | 620 | /** 621 | * Use removeTag instead. Will be removed in the future. 622 | * @deprecated 623 | */ 624 | removeProperty(element, propertyName) { 625 | return this.removeTag(element, propertyName); 626 | } 627 | 628 | /** 629 | * Remove a tag from a given element 630 | * @param {Object} element 631 | * @param {string} tagName 632 | * @return {Object} A new version of the element 633 | */ 634 | removeTag(element, tagName) { 635 | const newElement = simpleObjectDeepClone(element); 636 | const filteredTags = newElement.tag.filter(tag => tag.$.k !== tagName); 637 | 638 | newElement.tag = filteredTags; 639 | 640 | return newElement; 641 | } 642 | 643 | /** 644 | * Replace the coordinates of the OSM node and return a copy of the element 645 | * @param {Object} element 646 | * @param {number} lat 647 | * @param {number} lon 648 | * @return {Object} A new version of the element 649 | */ 650 | setCoordinates(element, lat, lon) { 651 | const newElement = simpleObjectDeepClone(element); 652 | newElement.$.lat = lat.toString(); 653 | newElement.$.lon = lon.toString(); 654 | 655 | return newElement; 656 | } 657 | 658 | /** 659 | * Get the nodes ids of the OSM way 660 | * @param {Object} way 661 | * @return {Array} nodeOsmIds 662 | */ 663 | getNodeIdsForWay(way) { 664 | return way.nd.map(node => `node/${node.$.ref}`); 665 | } 666 | 667 | /** 668 | * Replace the nodes of the OSM way and return a copy of the way 669 | * @param {Object} way 670 | * @param {Array} nodeOsmIds 671 | * @return {Object} A new version of the way 672 | */ 673 | setNodeIdsForWay(way, nodeOsmIds) { 674 | const newWay = simpleObjectDeepClone(way); 675 | newWay.nd = nodeOsmIds.map(id => ({ 676 | $: { 677 | ref: findElementId(id) 678 | } 679 | })); 680 | return newWay; 681 | } 682 | 683 | /** 684 | * Get the members objects from an OSM relation 685 | * @param {Object} relation 686 | * @return {Array} Array of object with keys id with osmId value e.g 'node/1234' and optional role key 687 | */ 688 | getRelationMembers(relation) { 689 | return relation.member.map(member => { 690 | const { type, ref, role } = member.$; 691 | const elementObjectCopy = { 692 | id: `${type}/${ref}` 693 | }; 694 | if (typeof role !== 'undefined') { 695 | elementObjectCopy.role = role; 696 | } 697 | return elementObjectCopy; 698 | }); 699 | } 700 | 701 | /** 702 | * Replace the members objects of the OSM relation and return a copy of the relation 703 | * @param {Object} relation 704 | * @param {Array} osmElements Array of object with keys id and optional role key. Key id contains an osmId value like 'node/1234' 705 | * @return {Object} A new version of the relation 706 | */ 707 | setRelationMembers(relation, osmElements) { 708 | const newRelation = simpleObjectDeepClone(relation); 709 | newRelation.member = osmElements.map(elementObject => { 710 | const { id, role } = elementObject; 711 | const elementType = findElementType(id); 712 | const elementId = findElementId(id); 713 | const elementObjectCopy = { 714 | type: elementType, 715 | ref: elementId 716 | }; 717 | if (typeof role !== 'undefined') { 718 | elementObjectCopy.role = elementObject.role; 719 | } 720 | return { 721 | $: elementObjectCopy 722 | }; 723 | }); 724 | return newRelation; 725 | } 726 | 727 | /** 728 | * Set the current UTC date to a given element 729 | * @param {Object} element 730 | * @return {Object} A new version of the element 731 | */ 732 | setTimestampToNow(element) { 733 | const newElement = simpleObjectDeepClone(element); 734 | newElement.$.timestamp = getCurrentIsoTimestamp(); 735 | 736 | return newElement; 737 | } 738 | 739 | /** 740 | * Change the version number (given by API) of an element 741 | * @param {Object} element 742 | * @param {int} version 743 | * @return {Object} A new version of the element 744 | */ 745 | setVersion(element, version) { 746 | const newElement = simpleObjectDeepClone(element); 747 | newElement.$.version = parseInt(version, 10).toString(); 748 | 749 | return newElement; 750 | } 751 | 752 | /** 753 | * Send an element to OSM 754 | * @param {Object} element 755 | * @param {number} changesetId 756 | * @return {Promise} 757 | */ 758 | sendElement(element, changesetId) { 759 | return sendElementRequest(this._auth, this.apiUrl, element, changesetId); 760 | } 761 | 762 | /** 763 | * Request to fetch all OSM elements within a bbox extent 764 | * @param {number} left The minimal longitude (X) 765 | * @param {number} bottom The minimal latitude (Y) 766 | * @param {number} right The maximal longitude (X) 767 | * @param {number} top The maximal latitude (Y) 768 | * @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 769 | * @return {Promise} 770 | */ 771 | fetchMapByBbox(left, bottom, right, top, mode = 'json') { 772 | return fetchMapByBboxRequest( 773 | this.apiUrl, 774 | left, 775 | bottom, 776 | right, 777 | top, 778 | mode, 779 | this._options.always_authenticated ? { auth: this._auth } : {} 780 | ); 781 | } 782 | 783 | /** 784 | * Delete an element from OSM 785 | * @param {Object} element 786 | * @param {number} changesetId 787 | * @return {Promise} Promise with the new version number due to deletion 788 | */ 789 | deleteElement(element, changesetId) { 790 | return deleteElementRequest(this._auth, this.apiUrl, element, changesetId); 791 | } 792 | 793 | /** 794 | * Get an user details 795 | * @param {string} userId The user ID 796 | * @return {Promise} Resolves on user details as JSON 797 | */ 798 | fetchUser(userId) { 799 | return fetchUserRequest( 800 | this.apiUrl, 801 | userId, 802 | this._options.always_authenticated ? { auth: this._auth } : {} 803 | ); 804 | } 805 | 806 | /** 807 | * Get all preferences from connected user 808 | * @return {Promise} Promise with Well formatted JSON of user preferences 809 | */ 810 | getUserPreferences() { 811 | return getUserPreferencesRequest(this._auth, this.apiUrl); 812 | } 813 | 814 | /** 815 | * Set all preferences for a connected user 816 | * @param {Object} object An object to provide keys values to create XML preferences 817 | * @return {Promise} Promise 818 | */ 819 | setUserPreferences(object) { 820 | return setUserPreferencesRequest(this._auth, this.apiUrl, object); 821 | } 822 | 823 | /** 824 | * Get a preference from a key for the connected user 825 | * @param {string} key The key to retrieve 826 | * @return {Promise} Promise with the value for the key 827 | */ 828 | getUserPreferenceByKey(key) { 829 | return getUserPreferenceByKeyRequest(this._auth, this.apiUrl, key); 830 | } 831 | 832 | /** 833 | * Set a preference from a key for the connected user 834 | * @param {string} key The key to set. 835 | * @param {string} value The value to set. Overwrite existing value if key exists 836 | * @return {Promise} Promise 837 | */ 838 | setUserPreferenceByKey(key, value) { 839 | return setUserPreferenceByKeyRequest(this._auth, this.apiUrl, key, value); 840 | } 841 | 842 | /** 843 | * Delete a preference from a key for the connected user 844 | * @param {string} key The key to use. 845 | * @return {Promise} Promise 846 | */ 847 | deleteUserPreference(key) { 848 | return deleteUserPreferenceRequest(this._auth, this.apiUrl, key); 849 | } 850 | } 851 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ### Table of Contents* [OsmRequest][1] 2 | * [Parameters][2] 3 | * [apiUrl][3] 4 | * [fetchNotes][4] 5 | * [Parameters][5] 6 | * [fetchNotesSearch][6] 7 | * [Parameters][7] 8 | * [fetchNote][8] 9 | * [Parameters][9] 10 | * [createNote][10] 11 | * [Parameters][11] 12 | * [commentNote][12] 13 | * [Parameters][13] 14 | * [closeNote][14] 15 | * [Parameters][15] 16 | * [reopenNote][16] 17 | * [Parameters][17] 18 | * [createChangeset][18] 19 | * [Parameters][19] 20 | * [isChangesetStillOpen][20] 21 | * [Parameters][21] 22 | * [fetchChangeset][22] 23 | * [Parameters][23] 24 | * [updateChangesetTags][24] 25 | * [Parameters][25] 26 | * [closeChangeset][26] 27 | * [Parameters][27] 28 | * [uploadChangesetOsc][28] 29 | * [Parameters][29] 30 | * [fetchChangesets][30] 31 | * [Parameters][31] 32 | * [createNodeElement][32] 33 | * [Parameters][33] 34 | * [createWayElement][34] 35 | * [Parameters][35] 36 | * [createRelationElement][36] 37 | * [Parameters][37] 38 | * [fetchElement][38] 39 | * [Parameters][39] 40 | * [fetchMultipleElements][40] 41 | * [Parameters][41] 42 | * [fetchRelationsForElement][42] 43 | * [Parameters][43] 44 | * [fetchWaysForNode][44] 45 | * [Parameters][45] 46 | * [findElementWithinOSMCollection][46] 47 | * [Parameters][47] 48 | * [getTags][48] 49 | * [Parameters][49] 50 | * [setProperty][50] 51 | * [Parameters][51] 52 | * [setTag][52] 53 | * [Parameters][53] 54 | * [setProperties][54] 55 | * [Parameters][55] 56 | * [setTags][56] 57 | * [Parameters][57] 58 | * [replaceTags][58] 59 | * [Parameters][59] 60 | * [removeProperty][60] 61 | * [Parameters][61] 62 | * [removeTag][62] 63 | * [Parameters][63] 64 | * [setCoordinates][64] 65 | * [Parameters][65] 66 | * [getNodeIdsForWay][66] 67 | * [Parameters][67] 68 | * [setNodeIdsForWay][68] 69 | * [Parameters][69] 70 | * [getRelationMembers][70] 71 | * [Parameters][71] 72 | * [setRelationMembers][72] 73 | * [Parameters][73] 74 | * [setTimestampToNow][74] 75 | * [Parameters][75] 76 | * [setVersion][76] 77 | * [Parameters][77] 78 | * [sendElement][78] 79 | * [Parameters][79] 80 | * [fetchMapByBbox][80] 81 | * [Parameters][81] 82 | * [deleteElement][82] 83 | * [Parameters][83] 84 | * [fetchUser][84] 85 | * [Parameters][85] 86 | * [getUserPreferences][86] 87 | * [setUserPreferences][87] 88 | * [Parameters][88] 89 | * [getUserPreferenceByKey][89] 90 | * [Parameters][90] 91 | * [setUserPreferenceByKey][91] 92 | * [Parameters][92] 93 | * [deleteUserPreference][93] 94 | * [Parameters][94] 95 | * [fetchElementRequest][95] 96 | * [Parameters][96] 97 | * [fetchElementRequestFull][97] 98 | * [Parameters][98] 99 | * [multiFetchElementsByTypeRequest][99] 100 | * [Parameters][100] 101 | * [fetchWaysForNodeRequest][101] 102 | * [Parameters][102] 103 | * [sendElementRequest][103] 104 | * [Parameters][104] 105 | * [fetchNotesRequest][105] 106 | * [Parameters][106] 107 | * [fetchNotesSearchRequest][107] 108 | * [Parameters][108] 109 | * [fetchNoteByIdRequest][109] 110 | * [Parameters][110] 111 | * [genericPostNoteRequest][111] 112 | * [Parameters][112] 113 | * [createNoteRequest][113] 114 | * [Parameters][114] 115 | * [createChangesetRequest][115] 116 | * [Parameters][116] 117 | * [changesetCheckRequest][117] 118 | * [Parameters][118] 119 | * [changesetGetRequest][119] 120 | * [Parameters][120] 121 | * [updateChangesetTagsRequest][121] 122 | * [Parameters][122] 123 | * [closeChangesetRequest][123] 124 | * [Parameters][124] 125 | * [uploadChangesetOscRequest][125] 126 | * [Parameters][126] 127 | * [fetchChangesetsRequest][127] 128 | * [Parameters][128] 129 | * [fetchMapByBboxRequest][129] 130 | * [Parameters][130] 131 | * [deleteElementRequest][131] 132 | * [Parameters][132] 133 | * [fetchRelationsForElementRequest][133] 134 | * [Parameters][134] 135 | * [fetchUserRequest][135] 136 | * [Parameters][136] 137 | * [getUserPreferencesRequest][137] 138 | * [Parameters][138] 139 | * [setUserPreferencesRequest][139] 140 | * [Parameters][140] 141 | * [getUserPreferenceByKeyRequest][141] 142 | * [Parameters][142] 143 | * [setUserPreferenceByKeyRequest][143] 144 | * [Parameters][144] 145 | * [deleteUserPreferenceRequest][145] 146 | * [Parameters][146]## OsmRequestOSM API request handlerType: [Object][147]### Parameters* `options` **[Object][147]?** Custom options to apply (optional, default `{}`)### apiUrlReturn the API URL to use for the requestsReturns **[string][148]** URL of the API### fetchNotesRetrieve the OSM notes in given bounding box#### Parameters* `left` **[number][149]** The minimal longitude (X) 147 | * `bottom` **[number][149]** The minimal latitude (Y) 148 | * `right` **[number][149]** The maximal longitude (X) 149 | * `top` **[number][149]** The maximal latitude (Y) 150 | * `limit` **[number][149]?** The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) (optional, default `null`) 151 | * `closedDays` **[number][149]?** 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) (optional, default `null`)Returns **[Promise][150]** Resolves on notes list### fetchNotesSearchFetch OSM notes with textual search#### Parameters* `q` **[string][148]** Specifies the search query 152 | * `format` **[string][148]?** It can be 'xml' (default) to get OSM 153 | and convert to JSON, 'raw' to return raw OSM XML, 'json' to 154 | return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS (optional, default `'xml'`) 155 | * `limit` **[number][149]?** The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) (optional, default `null`) 156 | * `closed` **[number][149]?** 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) (optional, default `null`) 157 | * `display_name` **[string][148]?** Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter (optional, default `null`) 158 | * `user` **[number][149]?** Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display\_name parameter (optional, default `null`) 159 | * `from` **[number][149]?** Specifies the beginning of a date range to search in for a note (optional, default `null`) 160 | * `to` **[number][149]?** Specifies the end of a date range to search in for a note. Today date is the default (optional, default `null`)Returns **[Promise][150]** ### fetchNoteGet OSM note by id 161 | param {number} noteId Identifier for the note#### Parameters* `noteId` 162 | * `format` **[string][148]** It can be 'xml' (default) to get OSM 163 | and convert to JSON, 'raw' to return raw OSM XML, 'json' to 164 | return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS (optional, default `'xml'`)Returns **[Promise][150]** ### createNoteCreate an OSM note#### Parameters* `lat` **[number][149]** Specifies the latitude of the note 165 | * `lon` **[number][149]** Specifies the longitude of the note 166 | * `text` **[string][148]** A mandatory text field with arbitrary text containing the noteReturns **[Promise][150]** ### commentNoteComment an OSM note#### Parameters* `noteId` 167 | * `text` **[string][148]** A mandatory text field with arbitrary text containing the noteReturns **[Promise][150]** ### closeNoteClose an OSM note#### Parameters* `noteId` 168 | * `text` **[string][148]** A mandatory text field with arbitrary text containing the noteReturns **[Promise][150]** ### reopenNoteReopen an OSM note#### Parameters* `noteId` 169 | * `text` **[string][148]** A mandatory text field with arbitrary text containing the noteReturns **[Promise][150]** ### createChangesetSend a request to OSM to create a new changeset#### Parameters* `createdBy` **[string][148]?** (optional, default `''`) 170 | * `comment` **[string][148]?** (optional, default `''`) 171 | * `tags` **[string][148]?** (optional, default `{}`)Returns **[Promise][150]** ### isChangesetStillOpenCheck if a changeset is still open#### Parameters* `changesetId` **[number][149]** Returns **[Promise][150]** ### fetchChangesetGet a changeset for a given id#### Parameters* `changesetId` **[number][149]** Returns **[Promise][150]** ### updateChangesetTagsUpdate changeset tags if still open#### Parameters* `changesetId` **[number][149]** 172 | * `createdBy` **[string][148]?** (optional, default `''`) 173 | * `comment` **[string][148]?** (optional, default `''`) 174 | * `object` **[Object][147]?** use to set multiples tags (optional, default `{}`)* Throws **any** Will throw an error for any request with http code 40xReturns **[Promise][150]** ### closeChangesetClose changeset for a given id if still opened#### Parameters* `changesetId` **[number][149]** * Throws **any** Will throw an error for any request with http code 40x.Returns **[Promise][150]** Empty string if it works### uploadChangesetOscUpload an OSC file content conforming to the OsmChange specification OSM changeset#### Parameters* `changesetId` **[string][148]** 175 | * `osmChangeContent` **[string][148]** OSC file content text* Throws **any** Will throw an error for any request with http code 40x.Returns **[Promise][150]** ### fetchChangesetsFetch changesets from OSM API#### Parameters* `options` **[Object][147]** Optional parameters 176 | 177 | * `options.left` **[number][149]?** The minimal longitude (X) 178 | * `options.bottom` **[number][149]?** The minimal latitude (Y) 179 | * `options.right` **[number][149]?** The maximal longitude (X) 180 | * `options.top` **[number][149]?** The maximal latitude (Y) 181 | * `options.display_name` **[string][148]?** Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter 182 | * `options.user` **[number][149]?** Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display\_name parameter 183 | * `options.time` **[string][148]?** 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][151] can parse. 184 | * `options.open` **[number][149]?** 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 185 | * `options.closed` **[number][149]?** Only finds changesets that are closed or have reached the element limit. Can be set to true 186 | * `options.changesets` **[number][149]?** Finds changesets with the specified idsReturns **[Promise][150]** ### createNodeElementCreate a shiny new OSM node element, in a JSON format#### Parameters* `lat` **[number][149]** 187 | * `lon` **[number][149]** 188 | * `tags` **[Object][147]?** Optional, initial tags (optional, default `{}`) 189 | * `id` **[string][148]?** Optional, identifier for OSM elementReturns **[Object][147]** ### createWayElementCreate a shiny new OSM way element, in a JSON format#### Parameters* `nodeOsmIds` **[Array][152]<[string][148]>** 190 | * `tags` **[Object][147]?** Optional, initial tags (optional, default `{}`) 191 | * `id` **[string][148]?** Optional, identifier for OSM elementReturns **[Object][147]** ### createRelationElementCreate a shiny new OSM relation element, in a JSON format#### Parameters* `osmElements` **[Array][152]<[Object][147]>** Array of object with keys id and optional role key. Key id contains an osmId value like 'node/1234' 192 | * `tags` **[Object][147]?** Optional, initial tags (optional, default `{}`) 193 | * `id` **[string][148]?** Optional, identifier for OSM elementReturns **[Object][147]** ### fetchElementFetch an OSM element by its ID and optionnally 194 | all other elements referenced by it#### Parameters* `osmId` **[string][148]** Eg: node/12345 195 | * `options` **[Object][147]** Optional parameters 196 | 197 | * `options.full` **[boolean][153]?** True for getting all elements referenced by this elementReturns **[Promise][150]** ### fetchMultipleElementsFetch multiple OSM elements by it full OSM IDs. Work only with a type of elements, no mixed elements type are allowed#### Parameters* `osmIds` **[Array][152]** Eg: \['node/12345', 'node/6789']. We do not support optional version e.g 'node/12345v2'Returns **[Promise][150]** ### fetchRelationsForElementFetch relation(s) from an OSM element#### Parameters* `osmId` **[string][148]** Eg: node/12345Returns **[Promise][150]** ### fetchWaysForNodeFetch ways using the given OSM node#### Parameters* `osmId` **[string][148]** Eg: node/12345Returns **[Promise][150]** Resolve on ways array (each one can be used as an Element for all other functions)### findElementWithinOSMCollectionFind an element with it OsmId within an OSM collection#### Parameters* `json` **[Object][147]** An object with key that can be 'node', 'way', 'relation' 198 | * `osmId` **[string][148]** Eg: node/12345Returns **Obejct** OSM element### getTagsGet all tags of this element as an object#### Parameters* `element` **[Object][147]** Returns **[Object][147]** Tags of this element### setPropertyUse setTag instead. Will be removed in the future.#### Parameters* `element` 199 | * `propertyName` 200 | * `propertyValue` **Meta*** **deprecated**: This is deprecated.### setTagAdd or replace a tag in a given element#### Parameters* `element` **[Object][147]** 201 | * `tagName` **[string][148]** 202 | * `tagValue` **[string][148]** Returns **[Object][147]** A new version of the element### setPropertiesUse setTags instead. Will be removed in the future.#### Parameters* `element` 203 | * `properties` **Meta*** **deprecated**: This is deprecated.### setTagsAdd or replace several tags in a given element#### Parameters* `element` **[Object][147]** 204 | * `tags` **[Object][147]** Returns **[Object][147]** A new version of the element### replaceTagsCompletely replace tags of a given element#### Parameters* `element` **[Object][147]** 205 | * `tags` **[Object][147]** The tags that will replace completely current element tagsReturns **[Object][147]** A new version of the element### removePropertyUse removeTag instead. Will be removed in the future.#### Parameters* `element` 206 | * `propertyName` **Meta*** **deprecated**: This is deprecated.### removeTagRemove a tag from a given element#### Parameters* `element` **[Object][147]** 207 | * `tagName` **[string][148]** Returns **[Object][147]** A new version of the element### setCoordinatesReplace the coordinates of the OSM node and return a copy of the element#### Parameters* `element` **[Object][147]** 208 | * `lat` **[number][149]** 209 | * `lon` **[number][149]** Returns **[Object][147]** A new version of the element### getNodeIdsForWayGet the nodes ids of the OSM way#### Parameters* `way` **[Object][147]** Returns **[Array][152]<[string][148]>** nodeOsmIds### setNodeIdsForWayReplace the nodes of the OSM way and return a copy of the way#### Parameters* `way` **[Object][147]** 210 | * `nodeOsmIds` **[Array][152]<[string][148]>** Returns **[Object][147]** A new version of the way### getRelationMembersGet the members objects from an OSM relation#### Parameters* `relation` **[Object][147]** Returns **[Array][152]<[Object][147]>** Array of object with keys id with osmId value e.g 'node/1234' and optional role key### setRelationMembersReplace the members objects of the OSM relation and return a copy of the relation#### Parameters* `relation` **[Object][147]** 211 | * `osmElements` **[Array][152]<[Object][147]>** Array of object with keys id and optional role key. Key id contains an osmId value like 'node/1234'Returns **[Object][147]** A new version of the relation### setTimestampToNowSet the current UTC date to a given element#### Parameters* `element` **[Object][147]** Returns **[Object][147]** A new version of the element### setVersionChange the version number (given by API) of an element#### Parameters* `element` **[Object][147]** 212 | * `version` **int** Returns **[Object][147]** A new version of the element### sendElementSend an element to OSM#### Parameters* `element` **[Object][147]** 213 | * `changesetId` **[number][149]** Returns **[Promise][150]** ### fetchMapByBboxRequest to fetch all OSM elements within a bbox extent#### Parameters* `left` **[number][149]** The minimal longitude (X) 214 | * `bottom` **[number][149]** The minimal latitude (Y) 215 | * `right` **[number][149]** The maximal longitude (X) 216 | * `top` **[number][149]** The maximal latitude (Y) 217 | * `mode` **[string][148]** The mode is json so output in the promise will be an object, otherwise, it will be an object and a XML string (optional, default `'json'`)Returns **[Promise][150]** ### deleteElementDelete an element from OSM#### Parameters* `element` **[Object][147]** 218 | * `changesetId` **[number][149]** Returns **[Promise][150]** Promise with the new version number due to deletion### fetchUserGet an user details#### Parameters* `userId` **[string][148]** The user IDReturns **[Promise][150]** Resolves on user details as JSON### getUserPreferencesGet all preferences from connected userReturns **[Promise][150]** Promise with Well formatted JSON of user preferences### setUserPreferencesSet all preferences for a connected user#### Parameters* `object` **[Object][147]** An object to provide keys values to create XML preferencesReturns **[Promise][150]** Promise### getUserPreferenceByKeyGet a preference from a key for the connected user#### Parameters* `key` **[string][148]** The key to retrieveReturns **[Promise][150]** Promise with the value for the key### setUserPreferenceByKeySet a preference from a key for the connected user#### Parameters* `key` **[string][148]** The key to set. 219 | * `value` **[string][148]** The value to set. Overwrite existing value if key existsReturns **[Promise][150]** Promise### deleteUserPreferenceDelete a preference from a key for the connected user#### Parameters* `key` **[string][148]** The key to use.Returns **[Promise][150]** Promise## fetchElementRequestRequest to fetch an OSM element### Parameters* `apiUrl` **[string][148]** The API URL 220 | * `osmId` **[string][148]** 221 | * `options` **[Object][147]?** Options (optional, default `{}`) 222 | 223 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Object][147]** ## fetchElementRequestFullRequest to fetch way or relation and all other elements referenced by it### Parameters* `apiUrl` **[string][148]** The API URL 224 | * `osmId` **[string][148]** Can only contain either a way or a relation 225 | * `options` **[Object][147]?** Options (optional, default `{}`) 226 | 227 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** Promise with well formatted JSON content## multiFetchElementsByTypeRequestRequest to fetch an OSM element### Parameters* `apiUrl` **[string][148]** The API URL 228 | * `osmIds` **[Array][152]** Eg: \['node/12345', 'node/6789']. We do not support optional version e.g 'node/12345v2' 229 | * `options` **[Object][147]?** Options (optional, default `{}`) 230 | 231 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## fetchWaysForNodeRequestRequest to fetch ways using the given OSM node### Parameters* `apiUrl` **[string][148]** The API URL 232 | * `osmId` **[string][148]** 233 | * `options` **[Object][147]?** Options (optional, default `{}`) 234 | 235 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Object][147]** ## sendElementRequestSend an element to OSM### Parameters* `auth` **osmAuth** An instance of osm-auth 236 | * `apiUrl` **[string][148]** The API URL 237 | * `element` **[Object][147]** 238 | * `changesetId` **[number][149]** Returns **[Promise][150]** ## fetchNotesRequestRequest to fetch OSM notes### Parameters* `apiUrl` **[string][148]** The API URL 239 | * `left` **[number][149]** The minimal longitude (X) 240 | * `bottom` **[number][149]** The minimal latitude (Y) 241 | * `right` **[number][149]** The maximal longitude (X) 242 | * `top` **[number][149]** The maximal latitude (Y) 243 | * `limit` **[number][149]?** The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) (optional, default `null`) 244 | * `closedDays` **[number][149]?** 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) (optional, default `null`) 245 | * `options` **[Object][147]?** Options (optional, default `{}`) 246 | 247 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Object][147]** ## fetchNotesSearchRequestRequest to get OSM notes with textual search### Parameters* `apiUrl` **[string][148]** The API URL 248 | * `q` **[string][148]** Specifies the search query 249 | * `format` **[string][148]?** It can be 'xml' (default) to get OSM 250 | and convert to JSON, 'raw' to return raw OSM XML, 'json' to 251 | return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS (optional, default `'xml'`) 252 | * `limit` **[number][149]?** The maximal amount of notes to retrieve (between 1 and 10000, defaults to 100) (optional, default `null`) 253 | * `closed` **[number][149]?** 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) (optional, default `null`) 254 | * `display_name` **[string][148]?** Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter (optional, default `null`) 255 | * `user` **[number][149]?** Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display\_name parameter (optional, default `null`) 256 | * `from` **[number][149]?** Specifies the beginning of a date range to search in for a note (optional, default `null`) 257 | * `to` **[number][149]?** Specifies the end of a date range to search in for a note. Today date is the default (optional, default `null`) 258 | * `options` **[Object][147]?** Options (optional, default `{}`) 259 | 260 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## fetchNoteByIdRequestRequest to fetch OSM note by id### Parameters* `apiUrl` **[string][148]** The API URL 261 | param {number} noteId Identifier for the note 262 | * `noteId` 263 | * `format` **[string][148]** It can be 'xml' (default) to get OSM 264 | and convert to JSON, 'raw' to return raw OSM XML, 'json' to 265 | return GeoJSON, 'gpx' to return GPX and 'rss' to return GeoRSS (optional, default `'xml'`) 266 | * `options` **[Object][147]?** Options (optional, default `{}`) 267 | 268 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## genericPostNoteRequestRequest generic enough to manage all POST request for a particular note### Parameters* `auth` **osmAuth** An instance of osm-auth 269 | * `apiUrl` **[string][148]** The API URL 270 | param {number} noteId Identifier for the note 271 | * `noteId` 272 | * `text` **[string][148]** A mandatory text field with arbitrary text containing the note 273 | * `type` **[string][148]** Mandatory type. It can be 'comment', 'close' or 'reopen'Returns **[Promise][150]** ## createNoteRequestRequest to create a note### Parameters* `auth` **osmAuth** An instance of osm-auth 274 | * `apiUrl` **[string][148]** The API URL 275 | * `lat` **[number][149]** Specifies the latitude of the note 276 | * `lon` **[number][149]** Specifies the longitude of the note 277 | * `text` **[string][148]** A mandatory text field with arbitrary text containing the noteReturns **[Promise][150]** ## createChangesetRequestRequest to create OSM changeset### Parameters* `auth` **osmAuth** An instance of osm-auth 278 | * `apiUrl` **[string][148]** The API URL 279 | * `createdBy` **[string][148]?** (optional, default `''`) 280 | * `comment` **[string][148]?** (optional, default `''`) 281 | * `tags` **[string][148]?** An object with keys values to set to tags (optional, default `{}`)Returns **[Promise][150]** ## changesetCheckRequestChecks if a given changeset is still opened at OSM.### Parameters* `apiUrl` **[string][148]** The API URL 282 | * `changesetId` **[number][149]** 283 | * `options` **[Object][147]?** Options (optional, default `{}`) 284 | 285 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## changesetGetRequestGet a changeset for a given id at OSM.### Parameters* `apiUrl` **[string][148]** The API URL 286 | * `changesetId` **[number][149]** 287 | * `options` **[Object][147]?** Options (optional, default `{}`) 288 | 289 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## updateChangesetTagsRequestUpdate tags if a given changeset is still opened at OSM.### Parameters* `auth` **osmAuth** An instance of osm-auth 290 | * `apiUrl` **[string][148]** The API URL 291 | * `changesetId` **[number][149]** 292 | * `createdBy` **[string][148]?** (optional, default `''`) 293 | * `comment` **[string][148]?** (optional, default `''`) 294 | * `tags` **[Object][147]?** Use to set multiples tags (optional, default `{}`)* Throws **any** Will throw an error for any request with http code 40x.Returns **[Promise][150]** ## closeChangesetRequestRequest to close changeset for a given id if still opened### Parameters* `auth` **osmAuth** An instance of osm-auth 295 | * `apiUrl` **[string][148]** The API URL 296 | * `changesetId` **[number][149]** * Throws **any** Will throw an error for any request with http code 40x.Returns **[Promise][150]** Empty string if it works## uploadChangesetOscRequestRequest to upload an OSC file content conforming to the OsmChange specification OSM changeset### Parameters* `auth` **osmAuth** An instance of osm-auth 297 | * `apiUrl` **[string][148]** The API URL 298 | * `changesetId` **[string][148]** 299 | * `osmChangeContent` **[string][148]** OSC file content textReturns **[Promise][150]** ## fetchChangesetsRequestRequest to get changesets from OSM API### Parameters* `apiUrl` **[string][148]** The API URL 300 | * `options` **[Object][147]** Optional parameters (optional, default `{}`) 301 | 302 | * `options.left` **[number][149]?** The minimal longitude (X) 303 | * `options.bottom` **[number][149]?** The minimal latitude (Y) 304 | * `options.right` **[number][149]?** The maximal longitude (X) 305 | * `options.top` **[number][149]?** The maximal latitude (Y) 306 | * `options.display_name` **[string][148]?** Specifies the creator of the returned notes by using a valid display name. Does not work together with the user parameter 307 | * `options.user` **[number][149]?** Specifies the creator of the returned notes by using a valid id of the user. Does not work together with the display\_name parameter 308 | * `options.time` **[string][148]?** 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][151] can parse. 309 | * `options.open` **[number][149]?** 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 310 | * `options.closed` **[number][149]?** Only finds changesets that are closed or have reached the element limit. Can be set to true 311 | * `options.changesets` **[number][149]?** Finds changesets with the specified ids 312 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## fetchMapByBboxRequestRequest to fetch all OSM elements within a bbox extent### Parameters* `apiUrl` **[string][148]** The API URL 313 | * `left` **[number][149]** The minimal longitude (X) 314 | * `bottom` **[number][149]** The minimal latitude (Y) 315 | * `right` **[number][149]** The maximal longitude (X) 316 | * `top` **[number][149]** The maximal latitude (Y) 317 | * `mode` **[string][148]** The mode is json so output in the promise will be an object, otherwise, it will be an object and a XML string (optional, default `'json'`) 318 | * `options` **[Object][147]?** Options (optional, default `{}`) 319 | 320 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## deleteElementRequestDelete an OSM element### Parameters* `auth` **osmAuth** An instance of osm-auth 321 | * `apiUrl` **[string][148]** The API URL 322 | * `element` **[Object][147]** 323 | * `changesetId` **[number][149]** Returns **[Promise][150]** Promise with the new version number due to deletion## fetchRelationsForElementRequestRequest to fetch relation(s) from an OSM element### Parameters* `apiUrl` **[string][148]** The API URL 324 | * `osmId` **[string][148]** 325 | * `options` **[Object][147]?** Options (optional, default `{}`) 326 | 327 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Promise][150]** ## fetchUserRequestRequest to fetch an OSM user details### Parameters* `apiUrl` **[string][148]** The API URL 328 | * `userId` **[string][148]** The user ID 329 | * `options` **[Object][147]?** Options (optional, default `{}`) 330 | 331 | * `options.auth` **[Object][147]?** Auth XHR object to use instead of unauthenticated callReturns **[Object][147]** ## getUserPreferencesRequestRequest to fetch preferences for the connected user### Parameters* `auth` **osmAuth** An instance of osm-auth 332 | * `apiUrl` **[string][148]** The API URL* Throws **any** Will throw an error for any request with http code 40x.Returns **[Promise][150]** Promise with the value for the key## setUserPreferencesRequestRequest to set all preferences for a connected user### Parameters* `auth` **osmAuth** An instance of osm-auth 333 | * `apiUrl` **[string][148]** The API URL 334 | * `object` **[Object][147]** An object to provide keys values to create XML preferencesReturns **[Promise][150]** Promise## getUserPreferenceByKeyRequestRequest to fetch a preference from a key for the connected user### Parameters* `auth` **osmAuth** An instance of osm-auth 335 | * `apiUrl` **[string][148]** The API URL 336 | * `key` **[string][148]** The key to retrieve* Throws **any** Will throw an error for any request with http code 40x.Returns **[Promise][150]** Promise with the value for the key## setUserPreferenceByKeyRequestRequest to set a preference from a key for the connected user### Parameters* `auth` **osmAuth** An instance of osm-auth 337 | * `apiUrl` **[string][148]** The API URL 338 | * `key` **[string][148]** The key to set 339 | * `value` **[string][148]** The value to set. Overwrite existing value if key existsReturns **[Promise][150]** Promise## deleteUserPreferenceRequestRequest to delete a preference from a key for the connected user### Parameters* `auth` **osmAuth** An instance of osm-auth 340 | * `apiUrl` **[string][148]** The API URL 341 | * `key` **[string][148]** The key to useReturns **[Promise][150]** Promise[1]: #osmrequest[2]: #parameters[3]: #apiurl[4]: #fetchnotes[5]: #parameters-1[6]: #fetchnotessearch[7]: #parameters-2[8]: #fetchnote[9]: #parameters-3[10]: #createnote[11]: #parameters-4[12]: #commentnote[13]: #parameters-5[14]: #closenote[15]: #parameters-6[16]: #reopennote[17]: #parameters-7[18]: #createchangeset[19]: #parameters-8[20]: #ischangesetstillopen[21]: #parameters-9[22]: #fetchchangeset[23]: #parameters-10[24]: #updatechangesettags[25]: #parameters-11[26]: #closechangeset[27]: #parameters-12[28]: #uploadchangesetosc[29]: #parameters-13[30]: #fetchchangesets[31]: #parameters-14[32]: #createnodeelement[33]: #parameters-15[34]: #createwayelement[35]: #parameters-16[36]: #createrelationelement[37]: #parameters-17[38]: #fetchelement[39]: #parameters-18[40]: #fetchmultipleelements[41]: #parameters-19[42]: #fetchrelationsforelement[43]: #parameters-20[44]: #fetchwaysfornode[45]: #parameters-21[46]: #findelementwithinosmcollection[47]: #parameters-22[48]: #gettags[49]: #parameters-23[50]: #setproperty[51]: #parameters-24[52]: #settag[53]: #parameters-25[54]: #setproperties[55]: #parameters-26[56]: #settags[57]: #parameters-27[58]: #replacetags[59]: #parameters-28[60]: #removeproperty[61]: #parameters-29[62]: #removetag[63]: #parameters-30[64]: #setcoordinates[65]: #parameters-31[66]: #getnodeidsforway[67]: #parameters-32[68]: #setnodeidsforway[69]: #parameters-33[70]: #getrelationmembers[71]: #parameters-34[72]: #setrelationmembers[73]: #parameters-35[74]: #settimestamptonow[75]: #parameters-36[76]: #setversion[77]: #parameters-37[78]: #sendelement[79]: #parameters-38[80]: #fetchmapbybbox[81]: #parameters-39[82]: #deleteelement[83]: #parameters-40[84]: #fetchuser[85]: #parameters-41[86]: #getuserpreferences[87]: #setuserpreferences[88]: #parameters-42[89]: #getuserpreferencebykey[90]: #parameters-43[91]: #setuserpreferencebykey[92]: #parameters-44[93]: #deleteuserpreference[94]: #parameters-45[95]: #fetchelementrequest[96]: #parameters-46[97]: #fetchelementrequestfull[98]: #parameters-47[99]: #multifetchelementsbytyperequest[100]: #parameters-48[101]: #fetchwaysfornoderequest[102]: #parameters-49[103]: #sendelementrequest[104]: #parameters-50[105]: #fetchnotesrequest[106]: #parameters-51[107]: #fetchnotessearchrequest[108]: #parameters-52[109]: #fetchnotebyidrequest[110]: #parameters-53[111]: #genericpostnoterequest[112]: #parameters-54[113]: #createnoterequest[114]: #parameters-55[115]: #createchangesetrequest[116]: #parameters-56[117]: #changesetcheckrequest[118]: #parameters-57[119]: #changesetgetrequest[120]: #parameters-58[121]: #updatechangesettagsrequest[122]: #parameters-59[123]: #closechangesetrequest[124]: #parameters-60[125]: #uploadchangesetoscrequest[126]: #parameters-61[127]: #fetchchangesetsrequest[128]: #parameters-62[129]: #fetchmapbybboxrequest[130]: #parameters-63[131]: #deleteelementrequest[132]: #parameters-64[133]: #fetchrelationsforelementrequest[134]: #parameters-65[135]: #fetchuserrequest[136]: #parameters-66[137]: #getuserpreferencesrequest[138]: #parameters-67[139]: #setuserpreferencesrequest[140]: #parameters-68[141]: #getuserpreferencebykeyrequest[142]: #parameters-69[143]: #setuserpreferencebykeyrequest[144]: #parameters-70[145]: #deleteuserpreferencerequest[146]: #parameters-71[147]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object[148]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String[149]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number[150]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise[151]: http://ruby-doc.org/stdlib-2.6.3/libdoc/date/rdoc/DateTime.html#method-c-parse[152]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array[153]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean 342 | --------------------------------------------------------------------------------