├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── examples ├── commentOnNote.md ├── createChangesetComment.md ├── createNote.md ├── deleteMessage.md ├── deletePreferences.md ├── dev-server.md ├── getApiCapabilities.md ├── getCapabilities.md ├── getChangeset.md ├── getChangesetDiff.md ├── getFeature.md ├── getFeatureAtVersion.md ├── getFeatureHistory.md ├── getFeatures.md ├── getMapData.md ├── getMessage.md ├── getNotesForArea.md ├── getNotesForQuery.md ├── getOwnUserBlocks.md ├── getPermissions.md ├── getPreferences.md ├── getRelationsForElement.md ├── getUIdFromDisplayName.md ├── getUser.md ├── getUserBlockById.md ├── getUsers.md ├── getWaysForNode.md ├── listChangesets.md ├── listMessages.md ├── reopenNote.md ├── sendMessage.md ├── subscribeToChangeset.md ├── subscribeToNote.md ├── type-safe-tags.md ├── unsubscribeFromChangeset.md ├── unsubscribeFromNote.md ├── updateMessageReadStatus.md ├── updatePreferences.md └── uploadChangeset.md ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── e2e.test.ts.snap │ ├── _createOsmChangeXml.test.ts │ ├── e2e.test.ts │ └── index.html ├── api │ ├── _osmFetch.ts │ ├── _rawResponse.d.ts │ ├── _xml.ts │ ├── changesets │ │ ├── __tests__ │ │ │ ├── _createOsmChangeXml.test.ts │ │ │ ├── _parseOsmChangeXml.test.ts │ │ │ ├── chunkOsmChange.test.ts │ │ │ └── uploadChangeset.test.ts │ │ ├── _createOsmChangeXml.ts │ │ ├── _parseOsmChangeXml.ts │ │ ├── chunkOsmChange.ts │ │ ├── createChangesetComment.ts │ │ ├── getChangesetDiff.ts │ │ ├── getChangesets.ts │ │ ├── index.ts │ │ ├── subscription.ts │ │ └── uploadChangeset.ts │ ├── getCapabilities.ts │ ├── getFeature.ts │ ├── getMapData.ts │ ├── getPermissions.ts │ ├── getUIdFromDisplayName.ts │ ├── getUser.ts │ ├── index.ts │ ├── messages │ │ ├── deleteMessage.ts │ │ ├── getMessage.ts │ │ ├── index.ts │ │ ├── listMessages.ts │ │ ├── sendMessage.ts │ │ └── updateMessageReadStatus.ts │ ├── notes │ │ ├── getNotes.ts │ │ ├── index.ts │ │ ├── noteActions.ts │ │ └── subscription.ts │ └── preferences │ │ ├── deletePreferences.ts │ │ ├── getPreferences.ts │ │ ├── index.ts │ │ └── updatePreferences.ts ├── auth │ ├── createPopup.ts │ ├── exchangeCode.ts │ ├── helpers.ts │ ├── index.ts │ ├── oauth2.ts │ └── types.ts ├── config.ts ├── index.ts └── types │ ├── changesets.ts │ ├── features.ts │ ├── general.ts │ ├── index.ts │ ├── messages.ts │ ├── notes.ts │ ├── osmPatch.ts │ └── user.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | id-token: write 10 | 11 | strategy: 12 | matrix: 13 | node-version: [18.x, 20.x, 21.x] 14 | 15 | steps: 16 | - name: ⏬ Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: 🔢 Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: ⏬ Install 25 | run: | 26 | npm install 27 | 28 | - name: ✨ Lint 29 | run: | 30 | npm run lint 31 | 32 | - name: 🔨 Build 33 | run: | 34 | npm run build 35 | 36 | - name: 🧪 Test 37 | run: | 38 | npm test 39 | env: 40 | FORCE_COLOR: 1 41 | 42 | # - name: 📈 Coveralls 43 | # uses: coverallsapp/github-action@1.1.3 44 | # with: 45 | # github-token: ${{ secrets.github_token }} 46 | 47 | - name: 📦 Publish 48 | if: ${{ github.ref == 'refs/heads/main' && matrix['node-version'] == '20.x' }} 49 | run: | 50 | npm config set //registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN 51 | npm run trypublish 52 | env: 53 | CI: true 54 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | dist 5 | temp 6 | yarn.lock 7 | __diff_output__ 8 | .jest* 9 | .env 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## 2.7.0 (2025-06-02) 11 | 12 | - Optionally support type-safe `Tags`. `Tags` is currently defined as `Record`. If you want additional type-safety, you can specify the keys are the allowed. See the docs for more info. 13 | 14 | ## 2.6.1 (2025-05-19) 15 | 16 | - Fix configuration issue which broke the type-definitions in the previous release 17 | 18 | ## 2.6.0 (2025-05-12) 19 | 20 | - When uploading a changeset, if you don't specify a `created_by` tag, this library will add one itself, so that changesets always have a `created_by` tag. 21 | - When uploading a changeset, the osmChange array is now automatically sorted, so that you don't have to worry about sorting in your own application. 22 | 23 | ## 2.5.1 (2025-05-05) 24 | 25 | - Fix bug causing OAuth not to work when using redirect-mode 26 | 27 | ## 2.5.0 (2025-04-02) 28 | 29 | - Allow custom HTTP headers or an `AbortSignal` to be passed to every API method. 30 | - Added 2 new functions to get user-blocks 31 | - Git repository moved to the [osmlab](https://github.com/osmlab) organisation. 32 | - Automatically split large changesets into chunks before uploading, if the changeset were to exceed the maximum number of features allowed by the API. 33 | 34 | ## 2.4.0 (2025-01-16) 35 | 36 | - Added a method to easily switch users (logout & log back in) 37 | 38 | ## 2.3.0 (2024-12-03) 39 | 40 | - Added 4 new functions for notes & changeset subscriptions 41 | 42 | ## 2.2.0 (2024-09-16) 43 | 44 | - Added 3 new functions for the preferences API 45 | - Added 5 new functions for the new messaging API 46 | - Added a new option `bbox` to `getNotesForQuery` 47 | - Added a new option `limit` to `listChangesets` 48 | - Added new function `getPermissions` 49 | - Added new function `getApiCapabilities` and deprecated `getCapabilities`. The new function uses the recently-released JSON API, which has a different format. 50 | 51 | ## 2.1.3 (2024-07-30) 52 | 53 | - Update dependencies to satisfy `npm audit` 54 | 55 | ## 2.1.2 (2024-06-30) 56 | 57 | - Fix crash when using the `getChangeset` API 58 | 59 | ## 2.1.1 (2024-02-18) 60 | 61 | - Fix bug in v2.1.0, and also apply new logic osmChange parser 62 | 63 | ## 2.1.0 (2024-02-17) 64 | 65 | - Change how changeset tags are embedded into osmChange files ([more info](https://community.osm.org/t/108670/8)) 66 | 67 | ## 2.0.0 (2024-01-25) 68 | 69 | - 💥 BREAKING CHANGE: Require nodejs v18 or newer. This allows the `fetch` polyfill to be removed. 70 | - (internal) modernise build infrastructure 71 | 72 | ## 1.0.6 (2024-01-24) 73 | 74 | - export type defintions for the OsmPatch format 75 | 76 | ## 1.0.5 (2022-09-10) 77 | 78 | - remove console.log and fix typedef 79 | 80 | ## 1.0.4 (2022-04-06) 81 | 82 | - fix bug with changeset xml 83 | 84 | ## 1.0.3 (2022-04-01) 85 | 86 | - fix bug with getRelationsForElement 87 | - update dependencies 88 | 89 | ## 1.0.2 (2022-01-30) 90 | 91 | - minors improvements to osmChange generation 92 | 93 | ## 1.0.1 (2021-12-24) 94 | 95 | - fix an issue with escaping XML characters when uploading changeset 96 | 97 | ## 1.0.0 (2021-12-20) 98 | 99 | - Initial release 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 osm-api-js Contributors 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenStreetMap API for Javascript 2 | 3 | [![Build Status](https://github.com/osmlab/osm-api-js/workflows/Build%20and%20Test/badge.svg)](https://github.com/osmlab/osm-api-js/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/osmlab/osm-api-js/badge.svg?branch=main&t=LQmPNl)](https://coveralls.io/github/osmlab/osm-api-js?branch=main) 5 | [![npm version](https://badge.fury.io/js/osm-api.svg)](https://badge.fury.io/js/osm-api) 6 | [![npm](https://img.shields.io/npm/dt/osm-api.svg)](https://www.npmjs.com/package/osm-api) 7 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/osm-api) 8 | 9 | 🗺️🌏 Javascript/Typescript wrapper around the OpenStreetMap API. 10 | 11 | Benefits: 12 | 13 | - Lightweight (24 kB gzipped) 14 | - works in nodejs and the browser. 15 | - converts OSM's XML into JSON automatically. 16 | - uses OAuth 2, so that you don't need to expose your OAuth `client_secret` 17 | 18 | ## Install 19 | 20 | ```sh 21 | npm install osm-api 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | const OSM = require("osm-api"); 28 | // or 29 | import * as OSM from "osm-api"; 30 | 31 | // you can call methods that don't require authentication 32 | await OSM.getFeature("way", 23906749); 33 | 34 | // Once you login, you can call methods that require authentication. 35 | // See the section below about authentication. 36 | await OSM.createChangesetComment(114733070, "Thanks for your edit!"); 37 | ``` 38 | 39 | If you don't use a bundler, you can also include the module using a ` 43 | 48 | ``` 49 | 50 | ## Examples 51 | 52 | All methods return promises. Examples requests and responses are available for all methods: 53 | 54 | > 🔑 means the method requires authentication 55 | 56 | - Features 57 | - [`getFeature()`](./examples/getFeature.md) 58 | - [`getFeatures()`](./examples/getFeatures.md) 59 | - [`getFeatureAtVersion`](./examples/getFeatureAtVersion.md) 60 | - [`getFeatureHistory`](./examples/getFeatureHistory.md) 61 | - [`getWaysForNode`](./examples/getWaysForNode.md) 62 | - [`getRelationsForElement`](./examples/getRelationsForElement.md) 63 | - Changesets 64 | - [`listChangesets`](./examples/listChangesets.md) 65 | - [`getChangeset`](./examples/getChangeset.md) 66 | - [`getChangesetDiff`](./examples/getChangesetDiff.md) 67 | - 🔑 [`uploadChangeset`](./examples/uploadChangeset.md) 68 | - 🔑 [`createChangesetComment`](./examples/createChangesetComment.md) 69 | - 🔑 [`subscribeToChangeset()`](./examples/subscribeToChangeset.md) 70 | - 🔑 [`unsubscribeFromChangeset()`](./examples/unsubscribeFromChangeset.md) 71 | - Users 72 | - [`getUser`](./examples/getUser.md) 73 | - [`getUsers`](./examples/getUsers.md) 74 | - [`getUIdFromDisplayName`](./examples/getUIdFromDisplayName.md) 75 | - [`getUserBlockById`](./examples/getUserBlockById.md) 76 | - 🔑 [`getOwnUserBlocks`](./examples/getOwnUserBlocks.md) 77 | - Messaging 78 | - 🔑 [`deleteMessage()`](./examples/deleteMessage.md) 79 | - 🔑 [`getMessage()`](./examples/getMessage.md) 80 | - 🔑 [`listMessages()`](./examples/listMessages.md) 81 | - 🔑 [`sendMessage()`](./examples/sendMessage.md) 82 | - 🔑 [`updateMessageReadStatus()`](./examples/updateMessageReadStatus.md) 83 | - Notes 84 | - [`getNotesForQuery()`](./examples/getNotesForQuery.md) 85 | - [`getNotesForArea()`](./examples/getNotesForArea.md) 86 | - [`createNote()`](./examples/createNote.md) 87 | - 🔑 [`commentOnNote()`](./examples/commentOnNote.md) 88 | - 🔑 [`reopenNote()`](./examples/reopenNote.md) 89 | - 🔑 [`subscribeToNote()`](./examples/subscribeToNote.md) 90 | - 🔑 [`unsubscribeFromNote()`](./examples/unsubscribeFromNote.md) 91 | - Preferences 92 | - 🔑 [`getPreferences()`](./examples/getPreferences.md) 93 | - 🔑 [`updatePreferences()`](./examples/updatePreferences.md) 94 | - 🔑 [`deletePreferences()`](./examples/deletePreferences.md) 95 | - Misc 96 | - [`getApiCapabilities()`](./examples/getApiCapabilities.md) 97 | - [`getMapData`](./examples/getMapData.md) 98 | - [Using the Development Server](./examples/dev-server.md) 99 | - [Additional type-safety for Keys/Tags](./examples/type-safe-tags.md) 100 | - Authentication (browser only, not available in NodeJS) 101 | - `login` 102 | - `logout` 103 | - `isLoggedIn` 104 | - 🔑 `getAuthToken()` 105 | - `authReady` 106 | - [`getPermissions()`](./examples/getPermissions.md) 107 | 108 | ## Authentication in the browser 109 | 110 | When used in the browser, this library lets you authenticate to OSM using OAuth 2. This requires either: 111 | 112 | 1. Redirecting the user to the OAuth page, or 113 | 2. Opening a popup window. 114 | 115 | ### 1. Popup 116 | 117 | If using a popup, you should create a separate landing page, such as `land.html`. This html file should contain the following code: 118 | 119 | > 💡 If you don't want to create a separate page, you can set the redirect URL to your 120 | > app's main page, as long as you include this HTML snippet. 121 | 122 | ```html 123 | 129 | ``` 130 | 131 | To login, or check whether the user is logged in, use the following code: 132 | 133 | ```js 134 | const OSM = require("osm-api"); 135 | 136 | OSM.login({ 137 | mode: "popup", 138 | clientId: ".......", 139 | redirectUrl: "https://example.com/land.html", 140 | // see the type definitions for other options 141 | }) 142 | .then(() => { 143 | console.log("User is now logged in!"); 144 | }) 145 | .catch(() => { 146 | console.log("User cancelled the login, or there was an error"); 147 | }); 148 | 149 | // you can check if a user is logged in using 150 | OSM.isLoggedIn(); 151 | 152 | // and you can get the access_token using 153 | OSM.getAuthToken(); 154 | ``` 155 | 156 | ### 2. Redirect 157 | 158 | If you use the redirect method, you don't need a separate landing page. 159 | 160 | ```js 161 | const OSM = require("osm-api"); 162 | 163 | // when you call this function, you will be immediately redirected to openstreetmap.org 164 | OSM.login({ 165 | mode: "redirect", 166 | clientId: ".......", 167 | redirectUrl: "https://example.com/land.html", 168 | // see the type definitions for other options 169 | }); 170 | ``` 171 | 172 | ```js 173 | const OSM = require("osm-api"); 174 | 175 | // If you login using the redirect method, you need to await 176 | // this promise before you can call `isLoggedIn` or `getAuthToken`. 177 | await OSM.authReady; 178 | 179 | // you can check if a user is logged in using 180 | OSM.isLoggedIn(); 181 | 182 | // and you can get the access_token using 183 | OSM.getAuthToken(); 184 | ``` 185 | 186 | ## Authentication in NodeJS 187 | 188 | In NodeJS, if you want to use a method that requires authentication, call the `configure()` function first: 189 | 190 | ```js 191 | const OSM = require("osm-api"); 192 | 193 | OSM.configure({ bearerToken: "..." }); 194 | // or 195 | OSM.configure({ basicAuth: { username: "...", password: "..." } }); 196 | 197 | // now you can call methods that require authentication. 198 | // Example: 199 | await OSM.createChangesetComment(114733070, "Thanks for your edit!"); 200 | ``` 201 | 202 | ## Comparison with osm-request 203 | 204 | This library offers several advantages over [osm-request](https://github.com/osmlab/osm-request): 205 | 206 | 1. **TypeScript support**: osm-api-js is built with TypeScript, providing better type safety and developer experience. 207 | 2. **Simpler API**: The API is designed to be more straightforward and easier to use. 208 | 3. **Smaller bundle size**: With fewer dependencies, osm-api-js has a noticeably smaller bundle size. 209 | 210 | While osm-request has been revived, osm-api-js was created when osm-request was abandoned and lacked OAuth 2 support. 211 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tsEslint from "typescript-eslint"; 2 | import config from "eslint-config-kyle"; 3 | 4 | export default tsEslint.config(...config, { 5 | rules: { 6 | "dot-notation": "off", 7 | quotes: ["error", "double"], 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/commentOnNote.md: -------------------------------------------------------------------------------- 1 | # commentOnNote 2 | 3 | ```ts 4 | import { commentOnNote } from "osm-api"; 5 | 6 | await commentOnNote(123456, "i also visited this café"); 7 | ``` 8 | 9 | Response: 10 | 11 | _void_ 12 | -------------------------------------------------------------------------------- /examples/createChangesetComment.md: -------------------------------------------------------------------------------- 1 | # createChangesetComment 2 | 3 | ```ts 4 | import { createChangesetComment } from "osm-api"; 5 | 6 | await createChangesetComment(1234567, "this is the comment"); 7 | ``` 8 | 9 | Response: _none_ 10 | -------------------------------------------------------------------------------- /examples/createNote.md: -------------------------------------------------------------------------------- 1 | # createNote 2 | 3 | ```ts 4 | import { createNote } from "osm-api"; 5 | 6 | await createNote(-36.880984, 174.738948, "there is a café here"); 7 | ``` 8 | 9 | Response: 10 | 11 | _void_ 12 | -------------------------------------------------------------------------------- /examples/deleteMessage.md: -------------------------------------------------------------------------------- 1 | # deleteMessage 2 | 3 | ```ts 4 | import { deleteMessage } from "osm-api"; 5 | 6 | await deleteMessage(1234567); 7 | ``` 8 | 9 | Response: 10 | 11 | _same as [`getMessage`](./getMessage.md)_ 12 | -------------------------------------------------------------------------------- /examples/deletePreferences.md: -------------------------------------------------------------------------------- 1 | # deletePreferences 2 | 3 | ```ts 4 | import { deletePreferences } from "osm-api"; 5 | 6 | await deletePreferences("key"); 7 | ``` 8 | 9 | Response: 10 | 11 | _none_ 12 | -------------------------------------------------------------------------------- /examples/dev-server.md: -------------------------------------------------------------------------------- 1 | # Using the Development Server 2 | 3 | For experimenting with the API, use the development server documented [here](https://osm.wiki/API_v0.6#URL_+_authentication). 4 | It uses a separate database with less data, allowing you to test without affecting the real database. 5 | 6 | 1. Create an account and application key on the [development server](https://master.apis.dev.openstreetmap.org/oauth2/applications). 7 | 8 | 2. Configure the library to use the development server: 9 | 10 | ```ts 11 | OSM.configure({ apiUrl: "https://master.apis.dev.openstreetmap.org" }); 12 | ``` 13 | 14 | Now you're ready to start making requests to the development server! 15 | -------------------------------------------------------------------------------- /examples/getApiCapabilities.md: -------------------------------------------------------------------------------- 1 | # getApiCapabilities 2 | 3 | ```ts 4 | import { getApiCapabilities } from "osm-api"; 5 | 6 | await getApiCapabilities(); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "attribution": "http://www.openstreetmap.org/copyright", 14 | "copyright": "OpenStreetMap and contributors", 15 | "generator": "OpenStreetMap server", 16 | "license": "http://opendatacommons.org/licenses/odbl/1-0/", 17 | "version": "0.6", 18 | "api": { 19 | "area": { 20 | "maximum": 0.25 21 | }, 22 | "changesets": { 23 | "default_query_limit": 100, 24 | "maximum_elements": 10000, 25 | "maximum_query_limit": 100 26 | }, 27 | "note_area": { 28 | "maximum": 25 29 | }, 30 | "notes": { 31 | "default_query_limit": 100, 32 | "maximum_query_limit": 10000 33 | }, 34 | "relationmembers": { 35 | "maximum": 32000 36 | }, 37 | "status": { 38 | "api": "online", 39 | "database": "online", 40 | "gpx": "online" 41 | }, 42 | "timeout": { 43 | "seconds": 300 44 | }, 45 | "tracepoints": { 46 | "per_page": 5000 47 | }, 48 | "version": { 49 | "maximum": "0.6", 50 | "minimum": "0.6" 51 | }, 52 | "waynodes": { 53 | "maximum": 2000 54 | } 55 | }, 56 | "policy": { 57 | "imagery": { 58 | "blacklist": [ 59 | { "regex": ".*\\.google(apis)?\\..*/.*" }, 60 | { "regex": "http://xdworld\\.vworld\\.kr:8080/.*" } 61 | ] 62 | } 63 | } 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /examples/getCapabilities.md: -------------------------------------------------------------------------------- 1 | # getCapabilities 2 | 3 | > [!CAUTION] 4 | > This method is deprecated, use [`getApiCapabilities`](./getApiCapabilities.md) instead. 5 | 6 | ```ts 7 | import { getCapabilities } from "osm-api"; 8 | 9 | await getCapabilities(); 10 | ``` 11 | 12 | Response: 13 | 14 | ```json 15 | { 16 | "limits": { 17 | "maxArea": 0.25, 18 | "maxChangesetElements": 10000, 19 | "maxNoteArea": 25, 20 | "maxTimeout": 300, 21 | "maxTracepointPerPage": 5000, 22 | "maxWayNodes": 2000 23 | }, 24 | "policy": { 25 | "imageryBlacklist": [] 26 | } 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/getChangeset.md: -------------------------------------------------------------------------------- 1 | # getChangeset 2 | 3 | ```ts 4 | import { getChangeset } from "osm-api"; 5 | 6 | await getChangeset(12345, /* includeDiscussion */ true); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "changes_count": 10, 14 | "closed_at": "2022-09-10T11:30:13.000Z", 15 | "comments_count": 0, 16 | "created_at": "2022-09-10T11:30:12.000Z", 17 | "id": 243637, 18 | "max_lat": -36.8804809, 19 | "max_lon": 174.7397571, 20 | "min_lat": -36.880627, 21 | "min_lon": 174.739496, 22 | "open": false, 23 | "tags": { 24 | "changesets_count": "5", 25 | "comment": "delete and redraw building", 26 | "created_by": "iD 2.21.1", 27 | "host": "https://openstreetmap.org/edit", 28 | "imagery_used": "LINZ NZ Aerial Imagery", 29 | "locale": "en-NZ" 30 | }, 31 | "uid": 12248, 32 | "user": "example_user" 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /examples/getChangesetDiff.md: -------------------------------------------------------------------------------- 1 | # getChangesetDiff 2 | 3 | ```ts 4 | import { getChangesetDiff } from "osm-api"; 5 | 6 | await getChangesetDiff(12345); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "create": [ 14 | { 15 | "changeset": 227200, 16 | "id": 4330788674, 17 | "lat": -36.8809317, 18 | "lon": 174.7397472, 19 | "tags": { 20 | "building": "house" 21 | }, 22 | "timestamp": "2021-12-16T09:29:15Z", 23 | "type": "node", 24 | "uid": 12248, 25 | "user": "example_user", 26 | "version": 1 27 | } 28 | /* see getFeature */ 29 | ], 30 | "delete": [ 31 | /* see getFeature */ 32 | ], 33 | "modify": [ 34 | /* see getFeature */ 35 | ] 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /examples/getFeature.md: -------------------------------------------------------------------------------- 1 | # getFeature 2 | 3 | ```ts 4 | import { getFeature } from "osm-api"; 5 | 6 | await getFeature("node", 1234); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "changeset": 243638, 14 | "id": 4305800016, 15 | "nodes": [4332338515, 4332338516, 4332338517, 4332338518, 4332338515], 16 | "tags": { 17 | "building": "house", 18 | "name:fr": "chez moi" 19 | }, 20 | "timestamp": "2022-09-10T11:47:13Z", 21 | "type": "way", 22 | "uid": 12248, 23 | "user": "example_user", 24 | "version": 4 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/getFeatureAtVersion.md: -------------------------------------------------------------------------------- 1 | # getFeatureAtVersion 2 | 3 | ```ts 4 | import { getFeatureAtVersion } from "osm-api"; 5 | 6 | await getFeatureAtVersion("node", 123456789, 5); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "changeset": 243638, 14 | "id": 4305800016, 15 | "nodes": [4332338515, 4332338516, 4332338517, 4332338518, 4332338515], 16 | "tags": { 17 | "building": "house", 18 | "name:fr": "chez moi" 19 | }, 20 | "timestamp": "2022-09-10T11:47:13Z", 21 | "type": "way", 22 | "uid": 12248, 23 | "user": "example_user", 24 | "version": 4 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/getFeatureHistory.md: -------------------------------------------------------------------------------- 1 | # getFeatureHistory 2 | 3 | ```ts 4 | import { getFeatureHistory } from "osm-api"; 5 | 6 | await getFeatureHistory("node", 1234); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "changeset": 243638, 15 | "id": 4305800016, 16 | "nodes": [4332338515, 4332338516, 4332338517, 4332338518, 4332338515], 17 | "tags": { 18 | "building": "house", 19 | "name:fr": "chez moi" 20 | }, 21 | "timestamp": "2022-09-10T11:47:13Z", 22 | "type": "way", 23 | "uid": 12248, 24 | "user": "example_user", 25 | "version": 4 26 | } 27 | ] 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/getFeatures.md: -------------------------------------------------------------------------------- 1 | # getFeatures 2 | 3 | ```ts 4 | import { getFeatures } from "osm-api"; 5 | 6 | await getFeatures("node", [1234, 1235]); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "changeset": 243638, 15 | "id": 4305800016, 16 | "nodes": [4332338515, 4332338516, 4332338517, 4332338518, 4332338515], 17 | "tags": { 18 | "building": "house", 19 | "name:fr": "chez moi" 20 | }, 21 | "timestamp": "2022-09-10T11:47:13Z", 22 | "type": "way", 23 | "uid": 12248, 24 | "user": "example_user", 25 | "version": 4 26 | } 27 | ] 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/getMapData.md: -------------------------------------------------------------------------------- 1 | # getMapData 2 | 3 | ```ts 4 | import { getMapData } from "osm-api"; 5 | 6 | await getMapData([ 7 | /* minLng */ 1, /* minLat */ 2, /* maxLng */ 3, /* maxLat */ 4, 8 | ]); 9 | ``` 10 | 11 | Response: 12 | 13 | ```json 14 | [ 15 | { 16 | "changeset": 243638, 17 | "id": 4305800016, 18 | "nodes": [4332338515, 4332338516, 4332338517, 4332338518, 4332338515], 19 | "tags": { 20 | "building": "house", 21 | "name:fr": "chez moi" 22 | }, 23 | "timestamp": "2022-09-10T11:47:13Z", 24 | "type": "way", 25 | "uid": 12248, 26 | "user": "example_user", 27 | "version": 4 28 | } 29 | ] 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/getMessage.md: -------------------------------------------------------------------------------- 1 | # getMessage 2 | 3 | ```ts 4 | import { getMessage } from "osm-api"; 5 | 6 | await getMessage(1234567); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "id": 1234567, 15 | "from_user_id": 111234, 16 | "from_display_name": "alice", 17 | "to_user_id": 111235, 18 | "to_display_name": "bob", 19 | "title": "hey buddy", 20 | "sent_on": "2024-05-01T09:41:00Z", 21 | "message_read": true, 22 | "deleted": false, 23 | "body_format": "markdown", 24 | "body": "On 2024-05-01 09:41:00 UTC alice wrote:\r\n\r\n sup bro, how u doing?" 25 | } 26 | ] 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/getNotesForArea.md: -------------------------------------------------------------------------------- 1 | # getNotesForArea 2 | 3 | ```ts 4 | import { getNotesForArea } from "osm-api"; 5 | 6 | const bbox = [174.738948, -36.880984, 174.741083, -36.880065]; 7 | 8 | await getNotesForArea(bbox); 9 | ``` 10 | 11 | Response: 12 | 13 | ```json 14 | [ 15 | { 16 | "close_url": "https://api.openstreetmap.org/api/0.6/notes/55251/close.json", 17 | "comment_url": "https://api.openstreetmap.org/api/0.6/notes/55251/comment.json", 18 | "comments": [ 19 | { 20 | "action": "opened", 21 | "date": "2024-01-24 06:33:43 UTC", 22 | "html": "

cycleway=no

", 23 | "text": "cycleway=no", 24 | "uid": 12248, 25 | "user": "kylenz_testing", 26 | "user_url": "https://api.openstreetmap.org/user/kylenz_testing" 27 | } 28 | ], 29 | "date_created": "2024-01-24 06:33:43 UTC", 30 | "id": 55251, 31 | "location": { 32 | "lat": -36.8300694, 33 | "lng": 174.7460568 34 | }, 35 | "status": "open", 36 | "url": "https://api.openstreetmap.org/api/0.6/notes/55251.json" 37 | } 38 | ] 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/getNotesForQuery.md: -------------------------------------------------------------------------------- 1 | # getNotesForQuery 2 | 3 | ```ts 4 | import { getNotesForQuery } from "osm-api"; 5 | 6 | await getNotesForQuery({ 7 | q: "cafés", 8 | // several other options are are available, see the type defintions for details 9 | }); 10 | ``` 11 | 12 | Response: 13 | 14 | ```json 15 | [ 16 | { 17 | "close_url": "https://api.openstreetmap.org/api/0.6/notes/55251/close.json", 18 | "comment_url": "https://api.openstreetmap.org/api/0.6/notes/55251/comment.json", 19 | "comments": [ 20 | { 21 | "action": "opened", 22 | "date": "2024-01-24 06:33:43 UTC", 23 | "html": "

cycleway=no

", 24 | "text": "cycleway=no", 25 | "uid": 12248, 26 | "user": "kylenz_testing", 27 | "user_url": "https://api.openstreetmap.org/user/kylenz_testing" 28 | } 29 | ], 30 | "date_created": "2024-01-24 06:33:43 UTC", 31 | "id": 55251, 32 | "location": { 33 | "lat": -36.8300694, 34 | "lng": 174.7460568 35 | }, 36 | "status": "open", 37 | "url": "https://api.openstreetmap.org/api/0.6/notes/55251.json" 38 | } 39 | ] 40 | ``` 41 | -------------------------------------------------------------------------------- /examples/getOwnUserBlocks.md: -------------------------------------------------------------------------------- 1 | # getOwnUserBlocks 2 | 3 | ```ts 4 | import { getOwnUserBlocks } from "osm-api"; 5 | 6 | await getOwnUserBlocks(); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "created_at": "2014-10-07T09:21:12Z", 15 | "ends_at": "2014-10-07T09:21:31Z", 16 | "id": 1, 17 | "needs_view": false, 18 | "reason": "test block", 19 | "creator": { "uid": 632, "user": "pnorman" }, 20 | "revoker": { "uid": 632, "user": "pnorman" }, 21 | "user": { "uid": 1154, "user": "SimonDev" }, 22 | "updated_at": "2014-10-07T09:21:31Z" 23 | } 24 | ] 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/getPermissions.md: -------------------------------------------------------------------------------- 1 | # getPermissions 2 | 3 | ```ts 4 | import { getPermissions } from "osm-api"; 5 | 6 | await getPermissions(); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "permissions": ["allow_write_api", "allow_read_gpx"] 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/getPreferences.md: -------------------------------------------------------------------------------- 1 | # getPreferences 2 | 3 | ```ts 4 | import { getPreferences } from "osm-api"; 5 | 6 | await getPreferences(); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "some key": "some value" 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/getRelationsForElement.md: -------------------------------------------------------------------------------- 1 | # getRelationsForElement 2 | 3 | ```ts 4 | import { getRelationsForElement } from "osm-api"; 5 | 6 | await getRelationsForElement("node", 123456789); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "changeset": 243638, 15 | "id": 4305800016, 16 | "members": [ 17 | { "ref": 4003, "role": "outer", "type": "way" }, 18 | { "ref": 4004, "role": "inner", "type": "way" } 19 | ], 20 | "tags": { 21 | "building": "house", 22 | "name:fr": "chez moi" 23 | }, 24 | "timestamp": "2022-09-10T11:47:13Z", 25 | "type": "relation", 26 | "uid": 12248, 27 | "user": "example_user", 28 | "version": 4 29 | } 30 | ] 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/getUIdFromDisplayName.md: -------------------------------------------------------------------------------- 1 | # getUIdFromDisplayName 2 | 3 | ```ts 4 | import { getUIdFromDisplayName } from "osm-api"; 5 | 6 | await getUIdFromDisplayName("example_user"); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | 12248 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/getUser.md: -------------------------------------------------------------------------------- 1 | # getUser 2 | 3 | ```ts 4 | import { getUser } from "osm-api"; 5 | 6 | await getUser(12248); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "account_created": "2021-12-15T09:49:18.000Z", 14 | "blocks": { 15 | "received": { 16 | "active": 0, 17 | "count": 0 18 | } 19 | }, 20 | "changesets": { 21 | "count": 15 22 | }, 23 | "contributor_terms": { 24 | "agreed": true 25 | }, 26 | "description": "", 27 | "display_name": "kylenz_testing", 28 | "id": 12248, 29 | "roles": [], 30 | "traces": { 31 | "count": 0 32 | } 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /examples/getUserBlockById.md: -------------------------------------------------------------------------------- 1 | # getUserBlockById 2 | 3 | ```ts 4 | import { getUserBlockById } from "osm-api"; 5 | 6 | await getUserBlockById(12345); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | { 13 | "created_at": "2014-10-07T09:21:12Z", 14 | "ends_at": "2014-10-07T09:21:31Z", 15 | "id": 1, 16 | "needs_view": false, 17 | "reason": "test block", 18 | "creator": { "uid": 632, "user": "pnorman" }, 19 | "revoker": { "uid": 632, "user": "pnorman" }, 20 | "user": { "uid": 1154, "user": "SimonDev" }, 21 | "updated_at": "2014-10-07T09:21:31Z" 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/getUsers.md: -------------------------------------------------------------------------------- 1 | # getUsers 2 | 3 | ```ts 4 | import { getUsers } from "osm-api"; 5 | 6 | await getUsers([12248]); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "account_created": "2021-12-15T09:49:18.000Z", 15 | "blocks": { 16 | "received": { 17 | "active": 0, 18 | "count": 0 19 | } 20 | }, 21 | "changesets": { 22 | "count": 15 23 | }, 24 | "contributor_terms": { 25 | "agreed": true 26 | }, 27 | "description": "", 28 | "display_name": "kylenz_testing", 29 | "id": 12248, 30 | "roles": [], 31 | "traces": { 32 | "count": 0 33 | } 34 | } 35 | ] 36 | ``` 37 | -------------------------------------------------------------------------------- /examples/getWaysForNode.md: -------------------------------------------------------------------------------- 1 | # getWaysForNode 2 | 3 | ```ts 4 | import { getWaysForNode } from "osm-api"; 5 | 6 | await getWaysForNode(123456789); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "changeset": 243638, 15 | "id": 4305800016, 16 | "nodes": [4332338515, 4332338516, 4332338517, 4332338518, 4332338515], 17 | "tags": { 18 | "building": "house", 19 | "name:fr": "chez moi" 20 | }, 21 | "timestamp": "2022-09-10T11:47:13Z", 22 | "type": "way", 23 | "uid": 12248, 24 | "user": "example_user", 25 | "version": 4 26 | } 27 | ] 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/listChangesets.md: -------------------------------------------------------------------------------- 1 | # listChangesets 2 | 3 | ```ts 4 | import { listChangesets } from "osm-api"; 5 | 6 | await listChangesets({ 7 | // for a list of possible queries, 8 | // see https://github.com/osmlab/osm-api-js/blob/5c3bd719/src/api/changesets/getChangesets.ts#L29-L48 9 | }); 10 | ``` 11 | 12 | Response: 13 | 14 | ```json 15 | [ 16 | { 17 | "changes_count": 10, 18 | "closed_at": "2022-09-10T11:30:13.000Z", 19 | "comments_count": 0, 20 | "created_at": "2022-09-10T11:30:12.000Z", 21 | "id": 243637, 22 | "max_lat": -36.8804809, 23 | "max_lon": 174.7397571, 24 | "min_lat": -36.880627, 25 | "min_lon": 174.739496, 26 | "open": false, 27 | "tags": { 28 | "changesets_count": "5", 29 | "comment": "delete and redraw building", 30 | "created_by": "iD 2.21.1", 31 | "host": "https://openstreetmap.org/edit", 32 | "imagery_used": "LINZ NZ Aerial Imagery", 33 | "locale": "en-NZ" 34 | }, 35 | "uid": 12248, 36 | "user": "example_user" 37 | } 38 | ] 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/listMessages.md: -------------------------------------------------------------------------------- 1 | # listMessages 2 | 3 | ```ts 4 | import { listMessages } from "osm-api"; 5 | 6 | await listMessages("inbox"); 7 | ``` 8 | 9 | Response: 10 | 11 | ```json 12 | [ 13 | { 14 | "id": 1234567, 15 | "from_user_id": 111234, 16 | "from_display_name": "alice", 17 | "to_user_id": 111235, 18 | "to_display_name": "bob", 19 | "title": "hey buddy", 20 | "sent_on": "2024-05-01T09:41:00Z", 21 | "message_read": true, 22 | "deleted": false, 23 | "body_format": "markdown" 24 | } 25 | ] 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/reopenNote.md: -------------------------------------------------------------------------------- 1 | # reopenNote 2 | 3 | ```ts 4 | import { reopenNote } from "osm-api"; 5 | 6 | await reopenNote(123456, "reopening because blah blah"); 7 | ``` 8 | 9 | Response: 10 | 11 | _void_ 12 | -------------------------------------------------------------------------------- /examples/sendMessage.md: -------------------------------------------------------------------------------- 1 | # sendMessage 2 | 3 | ```ts 4 | import { sendMessage } from "osm-api"; 5 | 6 | await sendMessage({ 7 | recipient: "bob", 8 | title: "hey buddy", 9 | body: "sup bro, how u doing?", 10 | }); 11 | ``` 12 | 13 | Response: 14 | 15 | _same as [`getMessage`](./getMessage.md)_ 16 | -------------------------------------------------------------------------------- /examples/subscribeToChangeset.md: -------------------------------------------------------------------------------- 1 | # subscribeToChangeset 2 | 3 | ```ts 4 | import { subscribeToChangeset } from "osm-api"; 5 | 6 | await subscribeToChangeset(123456); 7 | ``` 8 | 9 | Response: 10 | 11 | _void_ 12 | -------------------------------------------------------------------------------- /examples/subscribeToNote.md: -------------------------------------------------------------------------------- 1 | # subscribeToNote 2 | 3 | ```ts 4 | import { subscribeToNote } from "osm-api"; 5 | 6 | await subscribeToNote(123456); 7 | ``` 8 | 9 | Response: 10 | 11 | _void_ 12 | -------------------------------------------------------------------------------- /examples/type-safe-tags.md: -------------------------------------------------------------------------------- 1 | See the JSDoc comment for `Key` in [src/types/general.ts](../src/types/general.ts) 2 | -------------------------------------------------------------------------------- /examples/unsubscribeFromChangeset.md: -------------------------------------------------------------------------------- 1 | # unsubscribeFromChangeset 2 | 3 | ```ts 4 | import { unsubscribeFromChangeset } from "osm-api"; 5 | 6 | await unsubscribeFromChangeset(123456); 7 | ``` 8 | 9 | Response: 10 | 11 | _void_ 12 | -------------------------------------------------------------------------------- /examples/unsubscribeFromNote.md: -------------------------------------------------------------------------------- 1 | # unsubscribeFromNote 2 | 3 | ```ts 4 | import { unsubscribeFromNote } from "osm-api"; 5 | 6 | await unsubscribeFromNote(123456); 7 | ``` 8 | 9 | Response: 10 | 11 | _void_ 12 | -------------------------------------------------------------------------------- /examples/updateMessageReadStatus.md: -------------------------------------------------------------------------------- 1 | # updateMessageReadStatus 2 | 3 | ```ts 4 | import { updateMessageReadStatus } from "osm-api"; 5 | 6 | await updateMessageReadStatus(1234567, true); 7 | ``` 8 | 9 | Response: 10 | 11 | _same as [`getMessage`](./getMessage.md)_ 12 | -------------------------------------------------------------------------------- /examples/updatePreferences.md: -------------------------------------------------------------------------------- 1 | # updatePreferences 2 | 3 | ```ts 4 | import { updatePreferences } from "osm-api"; 5 | 6 | await updatePreferences("key", "value"); 7 | ``` 8 | 9 | Response: 10 | 11 | _none_ 12 | -------------------------------------------------------------------------------- /examples/uploadChangeset.md: -------------------------------------------------------------------------------- 1 | # uploadChangeset 2 | 3 | ```ts 4 | import { uploadChangeset } from "osm-api"; 5 | 6 | await uploadChangeset( 7 | { 8 | // tags 9 | created_by: "iD", 10 | comment: "change surface to unpaved", 11 | }, 12 | { 13 | // OsmDiff 14 | create: [ 15 | /* list of `OsmFeature`s */ 16 | ], 17 | modify: [], 18 | delete: [], 19 | } 20 | ); 21 | ``` 22 | 23 | Response: 24 | 25 | ```json 26 | 12345 27 | ``` 28 | 29 | (changeset number) 30 | 31 | ## Detailed Examples 32 | 33 | ### Updating existing features 34 | 35 | ```ts 36 | import { getFeature } from "osm-api"; 37 | 38 | const [feature] = await getFeature("node", 12345); 39 | 40 | feature.tags ||= {}; 41 | feature.tags.amenity = "restaurant"; 42 | 43 | await uploadChangeset( 44 | { created_by: "MyApp 1.0", comment: "tagging as resturant" }, 45 | { create: [], modify: [feature], delete: [] } 46 | ); 47 | ``` 48 | 49 | ### Creating new features 50 | 51 | To create a new node, several of the fields will have be be blanked out 52 | 53 | ```ts 54 | import { OsmNode } from "osm-api"; 55 | 56 | const newNode: OsmNode = { 57 | type: "node", 58 | lat: 123.456, 59 | lon: 789.123, 60 | tags: { 61 | amenity: "restaurant", 62 | }, 63 | id: -1, // Negative ID for new features 64 | 65 | changeset: -1, 66 | timestamp: "", 67 | uid: -1, 68 | user: "", 69 | version: 0, 70 | }; 71 | 72 | await uploadChangeset( 73 | { created_by: "MyApp 1.0", comment: "created a restaurant" }, 74 | { create: [newNode], modify: [], delete: [] } 75 | ); 76 | ``` 77 | 78 | ## Note about ordering 79 | 80 | When accessing the API directly, the order of items in `create`/`modify`/`delete` array matters. 81 | However, if you use this library, you don't need to worry about the order. 82 | This library will sort your changeset items before uploading it, so you send your data to this library in any order. 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osm-api", 3 | "version": "2.7.0", 4 | "contributors": [ 5 | "Kyle Hensel (https://github.com/k-yle)" 6 | ], 7 | "description": "🗺️🌏 Javascript/Typescript wrapper around the OpenStreetMap API", 8 | "main": "dist", 9 | "unpkg": "dist/index.min.js", 10 | "types": "dist/src/index.d.ts", 11 | "license": "MIT", 12 | "files": [ 13 | "dist" 14 | ], 15 | "keywords": [ 16 | "osm", 17 | "openstreetmap", 18 | "openstreetmap api" 19 | ], 20 | "repository": "https://github.com/osmlab/osm-api-js", 21 | "scripts": { 22 | "build": "rm -rf dist && tsc --emitDeclarationOnly && rm -rf dist/__tests__ && browserify -s OSM src/index.ts -p tsify > dist/index.js && uglifyjs dist/index.js > dist/index.min.js", 23 | "lint": "eslint .", 24 | "test": "vitest", 25 | "trypublish": "npm publish --provenance || true" 26 | }, 27 | "engines": { 28 | "node": ">=18" 29 | }, 30 | "dependencies": { 31 | "@types/geojson": "^7946.0.13", 32 | "fast-xml-parser": "^4.4.1" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^22.5.5", 36 | "browserify": "^17.0.0", 37 | "eslint": "^9.10.0", 38 | "eslint-config-kyle": "^25.0.0-beta3", 39 | "tsify": "^5.0.4", 40 | "typescript": "^5.6.2", 41 | "uglify-js": "^3.19.3", 42 | "vitest": "^2.1.1" 43 | }, 44 | "prettier": { 45 | "trailingComma": "es5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/e2e.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`end to end tests > APIs requiring authentication > getMessage 1`] = ` 4 | { 5 | "body": "\`markdown\` [formatting](https://example.com)", 6 | "body_format": "markdown", 7 | "deleted": false, 8 | "from_display_name": "kylenz_testing", 9 | "from_user_id": 12248, 10 | "id": 79, 11 | "message_read": false, 12 | "sent_on": "2024-09-16T11:56:40Z", 13 | "title": "helooooo", 14 | "to_display_name": "kylenz_testing", 15 | "to_user_id": 12248, 16 | } 17 | `; 18 | 19 | exports[`end to end tests > APIs requiring authentication > getOwnUserBlocks 1`] = `undefined`; 20 | 21 | exports[`end to end tests > APIs requiring authentication > getPreferences 1`] = `{}`; 22 | 23 | exports[`end to end tests > APIs requiring authentication > listMessages 1`] = ` 24 | [ 25 | { 26 | "body_format": "markdown", 27 | "deleted": false, 28 | "from_display_name": "kylenz_testing", 29 | "from_user_id": 12248, 30 | "id": 79, 31 | "message_read": false, 32 | "sent_on": "2024-09-16T11:56:40Z", 33 | "title": "helooooo", 34 | "to_display_name": "kylenz_testing", 35 | "to_user_id": 12248, 36 | }, 37 | ] 38 | `; 39 | 40 | exports[`end to end tests > getApiCapabilities 1`] = ` 41 | { 42 | "api": { 43 | "area": { 44 | "maximum": 0.25, 45 | }, 46 | "changesets": { 47 | "default_query_limit": 100, 48 | "maximum_elements": 10000, 49 | "maximum_query_limit": 100, 50 | }, 51 | "note_area": { 52 | "maximum": 25, 53 | }, 54 | "notes": { 55 | "default_query_limit": 100, 56 | "maximum_query_limit": 10000, 57 | }, 58 | "relationmembers": { 59 | "maximum": 32000, 60 | }, 61 | "status": { 62 | "api": "online", 63 | "database": "online", 64 | "gpx": "online", 65 | }, 66 | "timeout": { 67 | "seconds": 300, 68 | }, 69 | "tracepoints": { 70 | "per_page": 5000, 71 | }, 72 | "version": { 73 | "maximum": "0.6", 74 | "minimum": "0.6", 75 | }, 76 | "waynodes": { 77 | "maximum": 2000, 78 | }, 79 | }, 80 | "attribution": "http://www.openstreetmap.org/copyright", 81 | "copyright": "OpenStreetMap and contributors", 82 | "generator": "OpenStreetMap server", 83 | "license": "http://opendatacommons.org/licenses/odbl/1-0/", 84 | "policy": { 85 | "imagery": { 86 | "blacklist": [], 87 | }, 88 | }, 89 | "version": "0.6", 90 | } 91 | `; 92 | 93 | exports[`end to end tests > getCapabilities 1`] = ` 94 | { 95 | "limits": { 96 | "maxArea": 0.25, 97 | "maxChangesetElements": 10000, 98 | "maxNoteArea": 25, 99 | "maxTimeout": 300, 100 | "maxTracepointPerPage": 5000, 101 | "maxWayNodes": 2000, 102 | }, 103 | "policy": { 104 | "imageryBlacklist": [], 105 | }, 106 | } 107 | `; 108 | 109 | exports[`end to end tests > getChangeset (no discussion) 1`] = ` 110 | { 111 | "changes_count": 10, 112 | "closed_at": 2021-12-16T09:29:15.000Z, 113 | "comments_count": 2, 114 | "created_at": 2021-12-16T09:29:14.000Z, 115 | "discussion": undefined, 116 | "id": 227200, 117 | "max_lat": -36.8801278, 118 | "max_lon": 174.7400986, 119 | "min_lat": -36.8809762, 120 | "min_lon": 174.739496, 121 | "open": false, 122 | "tags": { 123 | "changesets_count": "2", 124 | "comment": "create node, way, and relation. modify way", 125 | "created_by": "iD 2.20.2", 126 | "host": "https://master.apis.dev.openstreetmap.org/edit", 127 | "imagery_used": "LINZ NZ Aerial Imagery", 128 | "locale": "en-NZ", 129 | "warnings:disconnected_way:highway": "1", 130 | }, 131 | "uid": 12248, 132 | "user": "kylenz_testing", 133 | } 134 | `; 135 | 136 | exports[`end to end tests > getChangeset (with discussion) 1`] = ` 137 | { 138 | "changes_count": 10, 139 | "closed_at": 2021-12-16T09:29:15.000Z, 140 | "comments_count": 2, 141 | "created_at": 2021-12-16T09:29:14.000Z, 142 | "discussion": [ 143 | { 144 | "date": 2024-06-30T08:59:15.000Z, 145 | "id": 736, 146 | "text": "­", 147 | "uid": "12248", 148 | "user": "kylenz_testing", 149 | "visible": true, 150 | }, 151 | { 152 | "date": 2024-06-30T08:59:55.000Z, 153 | "id": 737, 154 | "text": "sup bro", 155 | "uid": "12248", 156 | "user": "kylenz_testing", 157 | "visible": true, 158 | }, 159 | ], 160 | "id": 227200, 161 | "max_lat": -36.8801278, 162 | "max_lon": 174.7400986, 163 | "min_lat": -36.8809762, 164 | "min_lon": 174.739496, 165 | "open": false, 166 | "tags": { 167 | "changesets_count": "2", 168 | "comment": "create node, way, and relation. modify way", 169 | "created_by": "iD 2.20.2", 170 | "host": "https://master.apis.dev.openstreetmap.org/edit", 171 | "imagery_used": "LINZ NZ Aerial Imagery", 172 | "locale": "en-NZ", 173 | "warnings:disconnected_way:highway": "1", 174 | }, 175 | "uid": 12248, 176 | "user": "kylenz_testing", 177 | } 178 | `; 179 | 180 | exports[`end to end tests > getChangesetDiff 1`] = ` 181 | { 182 | "create": [ 183 | { 184 | "changeset": 227200, 185 | "id": 4330788673, 186 | "lat": -36.8804242, 187 | "lon": 174.7398822, 188 | "tags": { 189 | "bus": "yes", 190 | "highway": "bus_stop", 191 | "network": "AT", 192 | "network:wikidata": "Q4819567", 193 | "network:wikipedia": "en:Auckland Transport", 194 | "public_transport": "platform", 195 | }, 196 | "timestamp": "2021-12-16T09:29:15Z", 197 | "type": "node", 198 | "uid": 12248, 199 | "user": "kylenz_testing", 200 | "version": 1, 201 | }, 202 | { 203 | "changeset": 227200, 204 | "id": 4330788674, 205 | "lat": -36.8809317, 206 | "lon": 174.7397472, 207 | "tags": undefined, 208 | "timestamp": "2021-12-16T09:29:15Z", 209 | "type": "node", 210 | "uid": 12248, 211 | "user": "kylenz_testing", 212 | "version": 1, 213 | }, 214 | { 215 | "changeset": 227200, 216 | "id": 4330788675, 217 | "lat": -36.8801611, 218 | "lon": 174.7399983, 219 | "tags": undefined, 220 | "timestamp": "2021-12-16T09:29:15Z", 221 | "type": "node", 222 | "uid": 12248, 223 | "user": "kylenz_testing", 224 | "version": 1, 225 | }, 226 | { 227 | "changeset": 227200, 228 | "id": 4330788676, 229 | "lat": -36.8809762, 230 | "lon": 174.7397789, 231 | "tags": undefined, 232 | "timestamp": "2021-12-16T09:29:15Z", 233 | "type": "node", 234 | "uid": 12248, 235 | "user": "kylenz_testing", 236 | "version": 1, 237 | }, 238 | { 239 | "changeset": 227200, 240 | "id": 4330788677, 241 | "lat": -36.8809526, 242 | "lon": 174.7398473, 243 | "tags": undefined, 244 | "timestamp": "2021-12-16T09:29:15Z", 245 | "type": "node", 246 | "uid": 12248, 247 | "user": "kylenz_testing", 248 | "version": 1, 249 | }, 250 | { 251 | "changeset": 227200, 252 | "id": 4330788678, 253 | "lat": -36.8801278, 254 | "lon": 174.7400692, 255 | "tags": undefined, 256 | "timestamp": "2021-12-16T09:29:15Z", 257 | "type": "node", 258 | "uid": 12248, 259 | "user": "kylenz_testing", 260 | "version": 1, 261 | }, 262 | { 263 | "changeset": 227200, 264 | "id": 4330788679, 265 | "lat": -36.880182, 266 | "lon": 174.7400986, 267 | "tags": undefined, 268 | "timestamp": "2021-12-16T09:29:15Z", 269 | "type": "node", 270 | "uid": 12248, 271 | "user": "kylenz_testing", 272 | "version": 1, 273 | }, 274 | { 275 | "changeset": 227200, 276 | "id": 4305800031, 277 | "nodes": [ 278 | 4330788675, 279 | 4330788678, 280 | 4330788679, 281 | 4330788677, 282 | 4330788676, 283 | 4330788674, 284 | 4330788675, 285 | ], 286 | "tags": { 287 | "access": "no", 288 | "bus": "designated", 289 | "highway": "bus_guideway", 290 | "name": "The Loop", 291 | "oneway": "yes", 292 | }, 293 | "timestamp": "2021-12-16T09:29:15Z", 294 | "type": "way", 295 | "uid": 12248, 296 | "user": "kylenz_testing", 297 | "version": 1, 298 | }, 299 | { 300 | "changeset": 227200, 301 | "id": 4304961500, 302 | "members": [ 303 | { 304 | "ref": 4305800031, 305 | "role": "", 306 | "type": "way", 307 | }, 308 | ], 309 | "tags": { 310 | "name": "NX3", 311 | "network": "AT", 312 | "network:wikidata": "Q4819567", 313 | "network:wikipedia": "en:Auckland Transport", 314 | "route": "bus", 315 | "type": "route", 316 | }, 317 | "timestamp": "2021-12-16T09:29:15Z", 318 | "type": "relation", 319 | "uid": 12248, 320 | "user": "kylenz_testing", 321 | "version": 1, 322 | }, 323 | ], 324 | "delete": [], 325 | "modify": [ 326 | { 327 | "changeset": 227200, 328 | "id": 4305800016, 329 | "nodes": [ 330 | 4330788634, 331 | 4330788635, 332 | 4330788636, 333 | 4330788637, 334 | 4330788634, 335 | ], 336 | "tags": { 337 | "building": "yes", 338 | "building:levels": "1", 339 | "roof:colour": "grey", 340 | }, 341 | "timestamp": "2021-12-16T09:29:15Z", 342 | "type": "way", 343 | "uid": 12248, 344 | "user": "kylenz_testing", 345 | "version": 2, 346 | }, 347 | ], 348 | } 349 | `; 350 | 351 | exports[`end to end tests > getFeature full=false 1`] = ` 352 | [ 353 | { 354 | "changeset": 243638, 355 | "id": 4305800016, 356 | "nodes": [ 357 | 4332338515, 358 | 4332338516, 359 | 4332338517, 360 | 4332338518, 361 | 4332338515, 362 | ], 363 | "tags": { 364 | "building": "house", 365 | "name:fr": "chez moi", 366 | }, 367 | "timestamp": "2022-09-10T11:47:13Z", 368 | "type": "way", 369 | "uid": 12248, 370 | "user": "kylenz_testing", 371 | "version": 4, 372 | }, 373 | ] 374 | `; 375 | 376 | exports[`end to end tests > getFeature full=true 1`] = ` 377 | [ 378 | { 379 | "changeset": 243637, 380 | "id": 4332338515, 381 | "lat": -36.8806247, 382 | "lon": 174.7397153, 383 | "timestamp": "2022-09-10T11:30:13Z", 384 | "type": "node", 385 | "uid": 12248, 386 | "user": "kylenz_testing", 387 | "version": 1, 388 | }, 389 | { 390 | "changeset": 243637, 391 | "id": 4332338516, 392 | "lat": -36.8805369, 393 | "lon": 174.739748, 394 | "timestamp": "2022-09-10T11:30:13Z", 395 | "type": "node", 396 | "uid": 12248, 397 | "user": "kylenz_testing", 398 | "version": 1, 399 | }, 400 | { 401 | "changeset": 243637, 402 | "id": 4332338517, 403 | "lat": -36.8804862, 404 | "lon": 174.7395354, 405 | "timestamp": "2022-09-10T11:30:13Z", 406 | "type": "node", 407 | "uid": 12248, 408 | "user": "kylenz_testing", 409 | "version": 1, 410 | }, 411 | { 412 | "changeset": 243637, 413 | "id": 4332338518, 414 | "lat": -36.8805741, 415 | "lon": 174.7395027, 416 | "timestamp": "2022-09-10T11:30:13Z", 417 | "type": "node", 418 | "uid": 12248, 419 | "user": "kylenz_testing", 420 | "version": 1, 421 | }, 422 | { 423 | "changeset": 243638, 424 | "id": 4305800016, 425 | "nodes": [ 426 | 4332338515, 427 | 4332338516, 428 | 4332338517, 429 | 4332338518, 430 | 4332338515, 431 | ], 432 | "tags": { 433 | "building": "house", 434 | "name:fr": "chez moi", 435 | }, 436 | "timestamp": "2022-09-10T11:47:13Z", 437 | "type": "way", 438 | "uid": 12248, 439 | "user": "kylenz_testing", 440 | "version": 4, 441 | }, 442 | ] 443 | `; 444 | 445 | exports[`end to end tests > getNotesForQuery 1`] = ` 446 | [ 447 | { 448 | "close_url": "https://master.apis.dev.openstreetmap.org/api/0.6/notes/55251/close.json", 449 | "comment_url": "https://master.apis.dev.openstreetmap.org/api/0.6/notes/55251/comment.json", 450 | "comments": [ 451 | { 452 | "action": "opened", 453 | "date": "2024-01-24 06:33:43 UTC", 454 | "html": "

cycleway=no

", 455 | "text": "cycleway=no", 456 | "uid": 12248, 457 | "user": "kylenz_testing", 458 | "user_url": "https://master.apis.dev.openstreetmap.org/user/kylenz_testing", 459 | }, 460 | ], 461 | "date_created": "2024-01-24 06:33:43 UTC", 462 | "id": 55251, 463 | "location": { 464 | "lat": -36.8300694, 465 | "lng": 174.7460568, 466 | }, 467 | "status": "open", 468 | "url": "https://master.apis.dev.openstreetmap.org/api/0.6/notes/55251.json", 469 | }, 470 | ] 471 | `; 472 | 473 | exports[`end to end tests > getPermissions 1`] = ` 474 | { 475 | "attribution": "http://www.openstreetmap.org/copyright", 476 | "copyright": "OpenStreetMap and contributors", 477 | "generator": "OpenStreetMap server", 478 | "license": "http://opendatacommons.org/licenses/odbl/1-0/", 479 | "permissions": [], 480 | "version": "0.6", 481 | } 482 | `; 483 | 484 | exports[`end to end tests > getUser 1`] = ` 485 | { 486 | "account_created": 2021-12-15T09:49:18.000Z, 487 | "blocks": { 488 | "received": { 489 | "active": 0, 490 | "count": 0, 491 | }, 492 | }, 493 | "changesets": { 494 | "count": 27, 495 | }, 496 | "contributor_terms": { 497 | "agreed": true, 498 | }, 499 | "description": "", 500 | "display_name": "kylenz_testing", 501 | "id": 12248, 502 | "roles": [], 503 | "traces": { 504 | "count": 1, 505 | }, 506 | } 507 | `; 508 | 509 | exports[`end to end tests > getUserBlockById 1`] = ` 510 | { 511 | "created_at": "2014-10-07T09:21:12Z", 512 | "creator": { 513 | "uid": 632, 514 | "user": "pnorman", 515 | }, 516 | "ends_at": "2014-10-07T09:21:31Z", 517 | "id": 1, 518 | "needs_view": false, 519 | "reason": "test block", 520 | "revoker": { 521 | "uid": 632, 522 | "user": "pnorman", 523 | }, 524 | "updated_at": "2014-10-07T09:21:31Z", 525 | "user": { 526 | "uid": 1154, 527 | "user": "SimonDev", 528 | }, 529 | } 530 | `; 531 | 532 | exports[`end to end tests > listChangesets 1`] = ` 533 | [ 534 | { 535 | "changes_count": 2, 536 | "closed_at": 2022-09-10T11:47:14.000Z, 537 | "comments_count": 0, 538 | "created_at": 2022-09-10T11:47:13.000Z, 539 | "discussion": undefined, 540 | "id": 243638, 541 | "max_lat": -36.8804862, 542 | "max_lon": 174.739748, 543 | "min_lat": -36.8806247, 544 | "min_lon": 174.7395027, 545 | "open": false, 546 | "tags": { 547 | "comment": "Restore deleted features", 548 | "created_by": "OSM History Restorer", 549 | "host": "http://127.0.0.1:3000/#/restore-history", 550 | }, 551 | "uid": 12248, 552 | "user": "kylenz_testing", 553 | }, 554 | { 555 | "changes_count": 10, 556 | "closed_at": 2022-09-10T11:30:13.000Z, 557 | "comments_count": 0, 558 | "created_at": 2022-09-10T11:30:12.000Z, 559 | "discussion": undefined, 560 | "id": 243637, 561 | "max_lat": -36.8804809, 562 | "max_lon": 174.7397571, 563 | "min_lat": -36.880627, 564 | "min_lon": 174.739496, 565 | "open": false, 566 | "tags": { 567 | "changesets_count": "5", 568 | "comment": "delete and redraw building", 569 | "created_by": "iD 2.21.1", 570 | "host": "https://master.apis.dev.openstreetmap.org/edit", 571 | "imagery_used": "LINZ NZ Aerial Imagery", 572 | "locale": "en-NZ", 573 | }, 574 | "uid": 12248, 575 | "user": "kylenz_testing", 576 | }, 577 | { 578 | "changes_count": 10, 579 | "closed_at": 2021-12-16T09:29:15.000Z, 580 | "comments_count": 2, 581 | "created_at": 2021-12-16T09:29:14.000Z, 582 | "discussion": undefined, 583 | "id": 227200, 584 | "max_lat": -36.8801278, 585 | "max_lon": 174.7400986, 586 | "min_lat": -36.8809762, 587 | "min_lon": 174.739496, 588 | "open": false, 589 | "tags": { 590 | "changesets_count": "2", 591 | "comment": "create node, way, and relation. modify way", 592 | "created_by": "iD 2.20.2", 593 | "host": "https://master.apis.dev.openstreetmap.org/edit", 594 | "imagery_used": "LINZ NZ Aerial Imagery", 595 | "locale": "en-NZ", 596 | "warnings:disconnected_way:highway": "1", 597 | }, 598 | "uid": 12248, 599 | "user": "kylenz_testing", 600 | }, 601 | { 602 | "changes_count": 5, 603 | "closed_at": 2021-12-16T08:43:07.000Z, 604 | "comments_count": 0, 605 | "created_at": 2021-12-16T08:43:06.000Z, 606 | "discussion": undefined, 607 | "id": 227184, 608 | "max_lat": -36.8804809, 609 | "max_lon": 174.7397571, 610 | "min_lat": -36.880627, 611 | "min_lon": 174.739496, 612 | "open": false, 613 | "tags": { 614 | "changesets_count": "1", 615 | "comment": "add building in dev environment", 616 | "created_by": "iD 2.20.2", 617 | "host": "https://master.apis.dev.openstreetmap.org/edit", 618 | "imagery_used": "LINZ NZ Aerial Imagery", 619 | "locale": "en-NZ", 620 | }, 621 | "uid": 12248, 622 | "user": "kylenz_testing", 623 | }, 624 | ] 625 | `; 626 | -------------------------------------------------------------------------------- /src/__tests__/_createOsmChangeXml.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import type { OsmChange } from "../types"; 3 | import { 4 | createChangesetMetaXml, 5 | createOsmChangeXml, 6 | } from "../api/changesets/_createOsmChangeXml"; 7 | 8 | describe("createChangesetMetaXml", () => { 9 | it("generates the correct XML", () => { 10 | expect( 11 | createChangesetMetaXml({ 12 | comment: "micromapping my high school", 13 | created_by: "iD 2.20.0", 14 | }) 15 | ).toBe( 16 | ` 17 | 18 | 19 | 20 | 21 | 22 | ` 23 | ); 24 | }); 25 | }); 26 | 27 | describe("createOsmChangeXml", () => { 28 | it("generates the correct XML", () => { 29 | const osmChange = { 30 | create: [ 31 | { 32 | type: "node", 33 | id: -1, 34 | lat: -36.94949, 35 | lon: 174.7676, 36 | tags: { tourism: "information", information: "board" }, 37 | }, 38 | { 39 | type: "node", 40 | id: -3, 41 | lat: -36.91391836, 42 | lon: 174.76301235, 43 | }, 44 | ], 45 | modify: [ 46 | { 47 | type: "way", 48 | id: 4001, 49 | version: 6, 50 | tags: { 51 | amenity: "ice_cream", 52 | building: "yes", 53 | // test that 'constructor' works as a tag key since some editors don't 54 | // support it: https://github.com/openstreetmap/iD/issues/3044 55 | constructor: "Davies Construction Ltd", 56 | }, 57 | nodes: [3001, 3002, 3003, 3004, 3001], 58 | }, 59 | { 60 | type: "way", 61 | id: 4002, 62 | version: 2, 63 | tags: { highway: "path", surface: "< & \" ' >" }, 64 | nodes: [3005, 3006, 3007, -3, 3008, 3009, 3010], 65 | }, 66 | { 67 | type: "relation", 68 | id: 5001, 69 | version: 4, 70 | tags: { name: "Urban Route 15", type: "route" }, 71 | members: [ 72 | { ref: 4003, role: "outer", type: "way" }, 73 | { ref: 4004, role: "inner", type: "way" }, 74 | ], 75 | }, 76 | ], 77 | delete: [ 78 | { 79 | type: "node", 80 | id: 3011, 81 | version: 2, 82 | tags: { "addr:housenumber": "3", "addr:street": "Delaney Avenue" }, 83 | lat: -36.913179, 84 | lon: 174.7204874, 85 | }, 86 | ], 87 | } as unknown as OsmChange; 88 | expect(createOsmChangeXml(6001, osmChange)).toBe( 89 | ` 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ` 134 | ); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/__tests__/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from "vitest"; 2 | import { 3 | configure, 4 | getApiCapabilities, 5 | getCapabilities, 6 | getChangeset, 7 | getChangesetDiff, 8 | getFeature, 9 | getMessage, 10 | getNotesForQuery, 11 | getOwnUserBlocks, 12 | getPermissions, 13 | getPreferences, 14 | getUIdFromDisplayName, 15 | getUser, 16 | getUserBlockById, 17 | listChangesets, 18 | listMessages, 19 | } from ".."; 20 | 21 | /** 22 | * these tests call the real dev API. 23 | * 24 | * Only useful for the XML apis that require transformation 25 | */ 26 | 27 | const GribblehirstBbox = [ 28 | 174.738948, -36.880984, 174.741083, -36.880065, 29 | ]; 30 | 31 | const auth = process.env["OSM_AUTH"] || process.env["VITE_OSM_AUTH"]; 32 | 33 | describe("end to end tests", () => { 34 | beforeAll(() => { 35 | configure({ 36 | apiUrl: "https://master.apis.dev.openstreetmap.org", 37 | authHeader: auth, 38 | }); 39 | }); 40 | 41 | it("getApiCapabilities", async () => { 42 | expect(await getApiCapabilities()).toMatchSnapshot(); 43 | }); 44 | 45 | it("getCapabilities", async () => { 46 | expect(await getCapabilities()).toMatchSnapshot(); 47 | }); 48 | 49 | it("getPermissions", async () => { 50 | expect(await getPermissions()).toMatchSnapshot(); 51 | }); 52 | 53 | it.each` 54 | full 55 | ${true} 56 | ${false} 57 | `("getFeature full=$full", async ({ full }) => { 58 | expect(await getFeature("way", 4305800016, full)).toMatchSnapshot(); 59 | }); 60 | 61 | it("getUIdFromDisplayName", async () => { 62 | expect(await getUIdFromDisplayName("kylenz_testing")).toBe(12248); 63 | }); 64 | 65 | it("getUser", async () => { 66 | expect(await getUser(12248)).toMatchSnapshot(); 67 | }); 68 | 69 | it("listChangesets", async () => { 70 | expect( 71 | await listChangesets({ 72 | bbox: GribblehirstBbox, 73 | display_name: "kylenz_testing", 74 | }) 75 | ).toMatchSnapshot(); 76 | }); 77 | 78 | it("getChangeset (no discussion)", async () => { 79 | expect(await getChangeset(227200, false)).toMatchSnapshot(); 80 | }); 81 | 82 | it("getChangeset (with discussion)", async () => { 83 | expect(await getChangeset(227200, true)).toMatchSnapshot(); 84 | }); 85 | 86 | it("getChangesetDiff", async () => { 87 | expect(await getChangesetDiff(227200)).toMatchSnapshot(); 88 | }); 89 | 90 | it("getNotesForQuery", async () => { 91 | expect(await getNotesForQuery({ q: "cycleway" })).toMatchSnapshot(); 92 | }); 93 | 94 | it("getUserBlockById", async () => { 95 | expect(await getUserBlockById(1)).toMatchSnapshot(); 96 | }); 97 | 98 | (auth ? describe : describe.skip)("APIs requiring authentication", () => { 99 | it("listMessages", async () => { 100 | expect(await listMessages("outbox")).toMatchSnapshot(); 101 | }); 102 | 103 | it("getMessage", async () => { 104 | expect(await getMessage(79)).toMatchSnapshot(); 105 | }); 106 | 107 | it("getPreferences", async () => { 108 | expect(await getPreferences()).toMatchSnapshot(); 109 | }); 110 | 111 | it("getOwnUserBlocks", async () => { 112 | expect(await getOwnUserBlocks()).toMatchSnapshot(); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/__tests__/index.html: -------------------------------------------------------------------------------- 1 | 5 | 11 | 12 | 13 |
14 |
15 | You are logged in! 16 | 17 | 18 |

19 | 
20 | 23 | 24 | 67 | -------------------------------------------------------------------------------- /src/api/_osmFetch.ts: -------------------------------------------------------------------------------- 1 | import { getAuthToken } from "../auth"; 2 | import { getConfig } from "../config"; 3 | import { xmlParser } from "./_xml"; 4 | 5 | /** 6 | * extra options that are passed to {@link fetch}. Use this to 7 | * customise HTTP headers or pass an {@link AbortSignal}. 8 | */ 9 | export type FetchOptions = Omit; 10 | 11 | const toBase64 = (text: string): string => { 12 | if (typeof btoa === "undefined") { 13 | return Buffer.from(text, "binary").toString("base64"); 14 | } 15 | return btoa(text); 16 | }; 17 | 18 | /** @internal */ 19 | export async function osmFetch( 20 | path: string, 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | qsObject: Record | undefined, 23 | fetchOptions: RequestInit | undefined 24 | ): Promise { 25 | const { apiUrl, authHeader, basicAuth, userAgent } = getConfig(); 26 | 27 | const maybeBasicAuthHeader = 28 | basicAuth && 29 | `Basic ${toBase64(`${basicAuth.username}:${basicAuth.password}`)}`; 30 | 31 | const maybeOAuth2Token = getAuthToken() && `Bearer ${getAuthToken()}`; 32 | 33 | let qs = new URLSearchParams(qsObject).toString(); 34 | qs &&= `?${qs}`; 35 | 36 | const response = await fetch(`${apiUrl}/api${path}${qs}`, { 37 | ...fetchOptions, 38 | headers: { 39 | Authorization: 40 | authHeader || maybeBasicAuthHeader || maybeOAuth2Token || "", 41 | "User-Agent": userAgent, 42 | ...fetchOptions?.headers, 43 | }, 44 | }); 45 | 46 | const contentType = response.headers.get("Content-Type"); 47 | 48 | if (contentType?.startsWith("application/xml")) { 49 | const xml = await response.text(); 50 | const json = await xmlParser.parse(xml); 51 | return json as T; 52 | } 53 | 54 | if (contentType?.startsWith("application/json")) { 55 | return response.json(); 56 | } 57 | 58 | if (response.ok) { 59 | // some other content type, but not an error 60 | // (e.g. plaintext when creating a changeset) 61 | return (await response.text()) as unknown as T; 62 | } 63 | 64 | const error = new Error(`OSM API: ${await response.text()}`); 65 | error.cause = response.status; 66 | throw error; 67 | } 68 | -------------------------------------------------------------------------------- /src/api/_rawResponse.d.ts: -------------------------------------------------------------------------------- 1 | import type { FeatureCollection, Point } from "geojson"; 2 | import type { 3 | Changeset, 4 | ChangesetComment, 5 | OsmFeatureType, 6 | OsmNote, 7 | } from "../types"; 8 | 9 | /** @internal */ 10 | export type RawChangeset = Omit< 11 | Changeset, 12 | "discussion" | "created_at" | "closed_at" 13 | > & { 14 | created_at: string; 15 | closed_at?: string; 16 | comments?: (Omit & { 17 | /** ISO Date */ 18 | date: string; 19 | uid: number; 20 | })[]; 21 | }; 22 | 23 | /** @internal */ 24 | export type RawNotesSearch = FeatureCollection< 25 | Point, 26 | Omit 27 | >; 28 | 29 | /** @internal */ 30 | type RawFeature = { 31 | id: string; 32 | visible: string; 33 | version: string; 34 | changeset: string; 35 | timestamp: string; 36 | user: string; 37 | uid: string; 38 | }; 39 | 40 | /** @internal */ 41 | export type RawXmlTags = { tag?: { $: { k: string; v: string } }[] }; 42 | 43 | /** @internal */ 44 | type RawOsmChangeCategory = { 45 | node?: (RawXmlTags & { 46 | $: RawFeature & { lat: string; lon: string }; 47 | })[]; 48 | way?: (RawXmlTags & { 49 | $: RawFeature; 50 | nd?: { $: { ref: string } }[]; 51 | })[]; 52 | relation?: (RawXmlTags & { 53 | $: RawFeature; 54 | member?: { $: { type: OsmFeatureType; ref: string; role: string } }[]; 55 | })[]; 56 | }; 57 | 58 | /** @internal */ 59 | export type RawOsmChange = { 60 | osmChange: [ 61 | { 62 | create?: RawOsmChangeCategory[]; 63 | modify?: RawOsmChangeCategory[]; 64 | delete?: RawOsmChangeCategory[]; 65 | changeset?: [RawXmlTags]; 66 | }, 67 | ]; 68 | }; 69 | -------------------------------------------------------------------------------- /src/api/_xml.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from "fast-xml-parser"; 2 | 3 | /** @internal */ 4 | export const xmlParser = new XMLParser({ 5 | ignoreAttributes: false, 6 | attributesGroupName: "$", 7 | attributeNamePrefix: "", 8 | isArray: (tagName) => tagName !== "$", 9 | }); 10 | -------------------------------------------------------------------------------- /src/api/changesets/__tests__/_createOsmChangeXml.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createOsmChangeXml } from "../_createOsmChangeXml"; 3 | import type { OsmChange } from "../../../types"; 4 | 5 | const boilerplate = { 6 | changeset: 0, 7 | timestamp: "", 8 | uid: 0, 9 | user: "", 10 | version: 0, 11 | }; 12 | 13 | describe("_createOsmChangeXml", () => { 14 | it("generates XML from the JSON", () => { 15 | const osmChange: OsmChange = { 16 | create: [ 17 | { 18 | type: "node", 19 | id: -1, 20 | ...boilerplate, 21 | lat: -36, 22 | lon: 174, 23 | tags: { amenity: "cafe", name: "Café Contigo" }, 24 | }, 25 | ], 26 | modify: [ 27 | { 28 | type: "way", 29 | id: -2, 30 | ...boilerplate, 31 | nodes: [10, 11, 12, 13, 10], 32 | tags: { building: "yes" }, 33 | }, 34 | ], 35 | delete: [ 36 | { type: "node", id: 15, ...boilerplate, lat: 0, lon: 0 }, 37 | { type: "relation", id: 101, ...boilerplate, members: [] }, 38 | { type: "node", id: 16, ...boilerplate, lat: 0, lon: 0 }, 39 | { 40 | type: "way", 41 | id: 3, 42 | ...boilerplate, 43 | nodes: [15, 16, 17, 18, 15], 44 | tags: { building: "yes" }, 45 | }, 46 | { type: "node", id: 17, ...boilerplate, lat: 0, lon: 0 }, 47 | { type: "node", id: 18, ...boilerplate, lat: 0, lon: 0 }, 48 | ], 49 | }; 50 | const xml = createOsmChangeXml(123, osmChange, { 51 | created_by: "me", 52 | comment: "add café", 53 | }); 54 | expect(xml).toMatchInlineSnapshot(` 55 | " 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | " 93 | `); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/api/changesets/__tests__/_parseOsmChangeXml.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { parseOsmChangeXml } from "../_parseOsmChangeXml"; 3 | 4 | describe("_parseOsmChangeXml", () => { 5 | it("generates JSON from the XML", () => { 6 | const input = ` 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 | expect(parseOsmChangeXml(input)).toStrictEqual({ 32 | changeset: { comment: "add café", created_by: "me" }, 33 | create: [ 34 | { 35 | changeset: 123, 36 | id: -1, 37 | lat: -36, 38 | lon: 174, 39 | tags: { 40 | amenity: "cafe", 41 | name: "Café Contigo", 42 | }, 43 | timestamp: undefined, 44 | type: "node", 45 | uid: NaN, 46 | user: undefined, 47 | version: 0, 48 | }, 49 | ], 50 | delete: [], 51 | modify: [ 52 | { 53 | changeset: 123, 54 | id: -2, 55 | nodes: [10, 11, 12, 13, 10], 56 | tags: { 57 | building: "yes", 58 | }, 59 | timestamp: undefined, 60 | type: "way", 61 | uid: NaN, 62 | user: undefined, 63 | version: 0, 64 | }, 65 | ], 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/api/changesets/__tests__/chunkOsmChange.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import type { OsmChange, OsmFeature, OsmFeatureType } from "../../../types"; 3 | import { chunkOsmChange } from "../chunkOsmChange"; 4 | import type { ApiCapabilities } from "../../getCapabilities"; 5 | 6 | /** use with {@link Array.sort} to randomise the order */ 7 | const shuffle = () => 0.5 - Math.random(); 8 | 9 | const createMockFeatures = ( 10 | type: OsmFeatureType, 11 | count: number, 12 | _label: string 13 | ) => Array.from({ length: count }).fill({ type, _label }); 14 | 15 | const capabilities = { 16 | api: { changesets: { maximum_elements: 6 } }, 17 | }; 18 | 19 | describe("chunkOsmChange", () => { 20 | it("returns the input if the changeset is already small enough", () => { 21 | const input: OsmChange = { 22 | create: createMockFeatures("node", 3, "create"), 23 | modify: createMockFeatures("node", 1, "modify"), 24 | delete: createMockFeatures("node", 1, "delete"), 25 | }; 26 | expect(chunkOsmChange(input, capabilities)).toStrictEqual([input]); 27 | }); 28 | 29 | it("returns the input if the changeset has exactly the max number of items", () => { 30 | const input: OsmChange = { 31 | create: createMockFeatures("node", 3, "create"), 32 | modify: createMockFeatures("node", 1, "modify"), 33 | delete: createMockFeatures("node", 2, "delete"), 34 | }; 35 | expect(chunkOsmChange(input, capabilities)).toStrictEqual([input]); 36 | }); 37 | 38 | it("chunks features in a schematically valid order", () => { 39 | const input: OsmChange = { 40 | create: [ 41 | ...createMockFeatures("node", 4, "create"), 42 | ...createMockFeatures("way", 3, "create"), 43 | ...createMockFeatures("relation", 4, "create"), 44 | ].sort(shuffle), 45 | modify: [ 46 | ...createMockFeatures("node", 1, "modify"), 47 | ...createMockFeatures("way", 1, "modify"), 48 | ...createMockFeatures("relation", 1, "modify"), 49 | ].sort(shuffle), 50 | delete: [ 51 | ...createMockFeatures("node", 1, "delete"), 52 | ...createMockFeatures("way", 2, "delete"), 53 | ...createMockFeatures("relation", 3, "delete"), 54 | ].sort(shuffle), 55 | }; 56 | expect(chunkOsmChange(input, capabilities)).toStrictEqual([ 57 | // chunk 1: 58 | { 59 | create: [ 60 | ...createMockFeatures("node", 4, "create"), 61 | ...createMockFeatures("way", 2, "create"), 62 | ], 63 | modify: [], 64 | delete: [], 65 | }, 66 | // chunk 2: 67 | { 68 | create: [ 69 | ...createMockFeatures("way", 1, "create"), 70 | ...createMockFeatures("relation", 4, "create"), 71 | ], 72 | modify: [], 73 | delete: createMockFeatures("relation", 1, "delete"), 74 | }, 75 | // chunk 3: 76 | { 77 | create: [], 78 | modify: createMockFeatures("node", 1, "modify"), 79 | delete: [ 80 | ...createMockFeatures("relation", 2, "delete"), 81 | ...createMockFeatures("way", 2, "delete"), 82 | ...createMockFeatures("node", 1, "delete"), 83 | ], 84 | }, 85 | // chunk 4: 86 | { 87 | create: [], 88 | modify: [ 89 | ...createMockFeatures("way", 1, "modify"), 90 | ...createMockFeatures("relation", 1, "modify"), 91 | ], 92 | delete: [], 93 | }, 94 | ]); 95 | }); 96 | 97 | it("exposes the default limit to consumers", () => { 98 | expect(chunkOsmChange.DEFAULT_LIMIT).toBeTypeOf("number"); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/api/changesets/__tests__/uploadChangeset.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from "vitest"; 2 | import type { OsmChange, OsmFeature, OsmFeatureType } from "../../../types"; 3 | import { uploadChangeset } from "../uploadChangeset"; 4 | import { chunkOsmChange } from "../chunkOsmChange"; 5 | import { osmFetch } from "../../_osmFetch"; 6 | import { version } from "../../../../package.json"; 7 | 8 | let nextId = 0; 9 | vi.mock("../../_osmFetch", () => ({ osmFetch: vi.fn(() => ++nextId) })); 10 | 11 | /** use with {@link Array.sort} to randomise the order */ 12 | const shuffle = () => 0.5 - Math.random(); 13 | 14 | const createMockFeatures = ( 15 | type: OsmFeatureType, 16 | count: number, 17 | _label: string 18 | ) => 19 | Array.from({ length: count }).fill({ 20 | type, 21 | _label, 22 | nodes: [], 23 | members: [], 24 | }); 25 | 26 | describe("uploadChangeset", () => { 27 | beforeEach(() => { 28 | nextId = 0; 29 | vi.clearAllMocks(); 30 | chunkOsmChange.DEFAULT_LIMIT = 6; // don't do this in production 31 | }); 32 | 33 | const huge: OsmChange = { 34 | create: [ 35 | ...createMockFeatures("node", 4, "create"), 36 | ...createMockFeatures("way", 3, "create"), 37 | ...createMockFeatures("relation", 4, "create"), 38 | ].sort(shuffle), 39 | modify: [ 40 | ...createMockFeatures("node", 1, "modify"), 41 | ...createMockFeatures("way", 1, "modify"), 42 | ...createMockFeatures("relation", 1, "modify"), 43 | ].sort(shuffle), 44 | delete: [ 45 | ...createMockFeatures("node", 1, "delete"), 46 | ...createMockFeatures("way", 2, "delete"), 47 | ...createMockFeatures("relation", 3, "delete"), 48 | ].sort(shuffle), 49 | }; 50 | 51 | it("adds a fallback created_by tag", async () => { 52 | const output = await uploadChangeset( 53 | { comment: "added a building" }, 54 | { create: [], modify: [], delete: [] } 55 | ); 56 | expect(output).toBe(1); 57 | 58 | expect(osmFetch).toHaveBeenCalledTimes(3); 59 | expect(osmFetch).toHaveBeenNthCalledWith( 60 | 1, 61 | "/0.6/changeset/create", 62 | undefined, 63 | expect.objectContaining({ 64 | body: ` 65 | 66 | 67 | 68 | 69 | 70 | `, 71 | }) 72 | ); 73 | }); 74 | 75 | it("splits changesets into chunks and uploads them in a schematically valid order", async () => { 76 | const output = await uploadChangeset({ created_by: "me" }, huge); 77 | 78 | expect(osmFetch).toHaveBeenCalledTimes(12); 79 | 80 | for (const index of [0, 1, 2, 3]) { 81 | expect(osmFetch).toHaveBeenNthCalledWith( 82 | 1 + 3 * index, // 3 API requests per changeset 83 | "/0.6/changeset/create", 84 | undefined, 85 | expect.objectContaining({ 86 | body: ` 87 | 88 | 89 | 90 | 91 | 92 | `, 93 | }) 94 | ); 95 | } 96 | 97 | expect(output).toBe(1); 98 | }); 99 | 100 | it("splits changesets into chunks and suports a custom tag function", async () => { 101 | const output = await uploadChangeset({ created_by: "me" }, huge, { 102 | onChunk: ({ changesetIndex, changesetTotal, featureCount }) => ({ 103 | comment: "hiiii", 104 | created_by: "MyCoolApp", 105 | part: `${changesetIndex + 1} out of ${changesetTotal}`, 106 | totalSize: featureCount.toLocaleString("en"), 107 | }), 108 | }); 109 | 110 | expect(osmFetch).toHaveBeenCalledTimes(12); 111 | 112 | for (const index of [0, 1, 2, 3]) { 113 | expect(osmFetch).toHaveBeenNthCalledWith( 114 | 1 + 3 * index, // 3 API requests per changeset 115 | "/0.6/changeset/create", 116 | undefined, 117 | expect.objectContaining({ 118 | body: ` 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | `, 127 | }) 128 | ); 129 | } 130 | 131 | expect(output).toBe(1); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /src/api/changesets/_createOsmChangeXml.ts: -------------------------------------------------------------------------------- 1 | import { XMLBuilder } from "fast-xml-parser"; 2 | import type { OsmChange, OsmFeature, Tags } from "../../types"; 3 | 4 | const builder = new XMLBuilder({ 5 | ignoreAttributes: false, 6 | attributeNamePrefix: "$", 7 | format: true, 8 | suppressEmptyNode: true, 9 | suppressBooleanAttributes: false, 10 | }); 11 | 12 | /** @internal */ 13 | export function createChangesetMetaXml(tags: Tags) { 14 | return builder.build({ 15 | osm: { 16 | changeset: { 17 | tag: Object.entries(tags).map(([$k, $v]) => ({ $k, $v })), 18 | }, 19 | }, 20 | }); 21 | } 22 | 23 | const createGroup = ( 24 | csId: number, 25 | features: OsmFeature[], 26 | type?: keyof OsmChange 27 | ) => { 28 | const order = ["node", "way", "relation"]; 29 | // delete children before the features that reference them 30 | if (type === "delete") order.reverse(); 31 | 32 | return features.reduce( 33 | (ac, f) => { 34 | const base = { 35 | $id: f.id, 36 | $version: type === "create" ? 0 : f.version, 37 | $changeset: csId, 38 | tag: Object.entries(f.tags || {}).map(([$k, $v]) => ({ 39 | $k, 40 | $v, 41 | })), 42 | }; 43 | switch (f.type) { 44 | case "node": { 45 | const feat = { ...base, $lat: f.lat, $lon: f.lon }; 46 | return { ...ac, node: [...ac.node, feat] }; 47 | } 48 | case "way": { 49 | if (!f.nodes) throw new Error("Way has no nodes"); 50 | const feat = { ...base, nd: f.nodes.map(($ref) => ({ $ref })) }; 51 | return { ...ac, way: [...ac.way, feat] }; 52 | } 53 | case "relation": { 54 | if (!f.members) throw new Error("Relation has no members"); 55 | const feat = { 56 | ...base, 57 | member: f.members.map((m) => ({ 58 | $type: m.type, 59 | $ref: m.ref, 60 | $role: m.role, 61 | })), 62 | }; 63 | return { ...ac, relation: [...ac.relation, feat] }; 64 | } 65 | default: { 66 | return ac; 67 | } 68 | } 69 | }, 70 | // construct the object with the keys in the correct order 71 | Object.fromEntries(order.map((key) => [key, []])) as { 72 | node: unknown[]; 73 | way: unknown[]; 74 | relation: unknown[]; 75 | } 76 | ); 77 | }; 78 | 79 | /** 80 | * this function also sorts the elements to ensure that deletion 81 | * works. This means you don't need to worry about the array order 82 | * yourself. 83 | * For example, deleting a square building involves deleting 4 nodes 84 | * and 1 way. The 4 nodes need to be included in the deletions array 85 | * before the way. 86 | */ 87 | // not marked as internal - this one can be used by consumers 88 | export function createOsmChangeXml( 89 | csId: number, 90 | diff: OsmChange, 91 | metadata?: Tags 92 | ): string { 93 | return builder.build({ 94 | osmChange: { 95 | $version: "0.6", 96 | $generator: "osm-api-js", 97 | changeset: metadata 98 | ? { tag: Object.entries(metadata).map(([$k, $v]) => ({ $k, $v })) } 99 | : undefined, 100 | create: [createGroup(csId, diff.create, "create")], 101 | modify: [createGroup(csId, diff.modify, "modify")], 102 | delete: [ 103 | { "$if-unused": true, ...createGroup(csId, diff.delete, "delete") }, 104 | ], 105 | }, 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /src/api/changesets/_parseOsmChangeXml.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | OsmChange, 3 | OsmFeature, 4 | OsmNode, 5 | OsmRelation, 6 | OsmWay, 7 | Tags, 8 | } from "../../types"; 9 | import type { 10 | RawOsmChange, 11 | RawOsmChangeCategory, 12 | RawXmlTags, 13 | } from "../_rawResponse"; 14 | import { xmlParser } from "../_xml"; 15 | 16 | function parseTags(feat: RawXmlTags | undefined): Tags | undefined { 17 | if (!feat?.tag) return undefined; 18 | return Object.fromEntries(feat.tag.map((tag) => [tag.$.k, tag.$.v])); 19 | } 20 | 21 | function common(feat: NonNullable[0]) { 22 | return { 23 | changeset: +feat.$.changeset, 24 | id: +feat.$.id, 25 | timestamp: feat.$.timestamp, 26 | uid: +feat.$.uid, 27 | user: feat.$.user, 28 | version: +feat.$.version, 29 | tags: parseTags(feat), 30 | }; 31 | } 32 | 33 | const mapSection = (c: RawOsmChangeCategory): OsmFeature[] => { 34 | const feats: OsmFeature[] = []; 35 | 36 | if (c.node) { 37 | const nodes: OsmNode[] = c.node.map((n) => ({ 38 | type: "node", 39 | ...common(n), 40 | lat: +n.$.lat, 41 | lon: +n.$.lon, 42 | })); 43 | feats.push(...nodes); 44 | } 45 | 46 | if (c.way) { 47 | const ways: OsmWay[] = c.way.map((w) => ({ 48 | type: "way", 49 | ...common(w), 50 | nodes: w.nd?.map((n) => +n.$.ref) || [], 51 | })); 52 | feats.push(...ways); 53 | } 54 | 55 | if (c.relation) { 56 | const relations: OsmRelation[] = c.relation.map((r) => ({ 57 | type: "relation", 58 | ...common(r), 59 | members: 60 | r.member?.map((m) => ({ 61 | ref: +m.$.ref, 62 | role: m.$.role, 63 | type: m.$.type, 64 | })) || [], 65 | })); 66 | feats.push(...relations); 67 | } 68 | 69 | return feats; 70 | }; 71 | 72 | /** @internal */ 73 | export function parseOsmChangeJson(raw: RawOsmChange) { 74 | const changesetTags = parseTags(raw.osmChange[0].changeset?.[0]); 75 | 76 | const diff: OsmChange & { changeset?: Tags } = { 77 | create: raw.osmChange[0].create?.flatMap(mapSection) || [], 78 | modify: raw.osmChange[0].modify?.flatMap(mapSection) || [], 79 | delete: raw.osmChange[0].delete?.flatMap(mapSection) || [], 80 | }; 81 | if (changesetTags) diff.changeset = changesetTags; 82 | 83 | return diff; 84 | } 85 | 86 | // not marked as internal - this one can be used by consumers 87 | export function parseOsmChangeXml( 88 | xml: string 89 | ): OsmChange & { changeset?: Tags } { 90 | const raw = xmlParser.parse(xml); 91 | return parseOsmChangeJson(raw); 92 | } 93 | -------------------------------------------------------------------------------- /src/api/changesets/chunkOsmChange.ts: -------------------------------------------------------------------------------- 1 | import type { OsmChange, OsmFeature, OsmFeatureType } from "../../types"; 2 | import type { ApiCapabilities } from "../getCapabilities"; 3 | 4 | const ACTIONS = ([ 5 | "create", 6 | "modify", 7 | "delete", 8 | ]) satisfies (keyof OsmChange)[]; 9 | 10 | type Action = (typeof ACTIONS)[number]; 11 | type Group = `${Action}-${OsmFeatureType}`; 12 | 13 | /** to ensure the uploads are valid, we must follow this order */ 14 | const UPLOAD_ORDER: Group[] = [ 15 | "create-node", 16 | "create-way", 17 | "create-relation", 18 | "delete-relation", 19 | "delete-way", 20 | "delete-node", 21 | "modify-node", 22 | "modify-way", 23 | "modify-relation", 24 | ]; 25 | 26 | const EMPTY_CHANGESET = (): OsmChange => ({ 27 | create: [], 28 | modify: [], 29 | delete: [], 30 | }); 31 | 32 | /** @internal */ 33 | export function getOsmChangeSize(osmChange: OsmChange) { 34 | return ( 35 | osmChange.create.length + osmChange.modify.length + osmChange.delete.length 36 | ); 37 | } 38 | 39 | /** 40 | * If a changeset is too big to upload at once, this function can split 41 | * the changeset into smaller chunks, which can be uploaded separately. 42 | * 43 | * @param capabilities - optional, this data can be fetched from `getApiCapabilities`. 44 | * if not supplied, {@link chunkOsmChange.DEFAULT_LIMIT} is used. 45 | */ 46 | export function chunkOsmChange( 47 | osmChange: OsmChange, 48 | capabilities?: ApiCapabilities 49 | ): OsmChange[] { 50 | const max = 51 | capabilities?.api.changesets.maximum_elements ?? 52 | chunkOsmChange.DEFAULT_LIMIT; 53 | 54 | // abort early if there's nothing to do. 55 | if (getOsmChangeSize(osmChange) <= max) return [osmChange]; 56 | 57 | const grouped: Partial> = {}; 58 | 59 | for (const action of ACTIONS) { 60 | for (const feature of osmChange[action]) { 61 | const group: Group = `${action}-${feature.type}`; 62 | 63 | grouped[group] ||= []; 64 | grouped[group].push(feature); 65 | } 66 | } 67 | 68 | const chunks: OsmChange[] = [EMPTY_CHANGESET()]; 69 | 70 | function getNext() { 71 | for (const group of UPLOAD_ORDER) { 72 | const action = group.split("-")[0]; 73 | const feature = grouped[group]?.pop(); 74 | if (feature) return { action, feature }; 75 | } 76 | return undefined; 77 | } 78 | 79 | let next: ReturnType; 80 | while ((next = getNext())) { 81 | const head = chunks[0]; 82 | 83 | head[next.action].push(next.feature); 84 | 85 | // if the changeset is now too big, create a new chunk 86 | if (getOsmChangeSize(head) >= max) { 87 | chunks.unshift(EMPTY_CHANGESET()); 88 | } 89 | } 90 | 91 | return chunks.reverse(); 92 | } 93 | 94 | chunkOsmChange.DEFAULT_LIMIT = 10_000; 95 | -------------------------------------------------------------------------------- /src/api/changesets/createChangesetComment.ts: -------------------------------------------------------------------------------- 1 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 2 | 3 | /** Add a comment to a changeset. The changeset must be closed. */ 4 | export async function createChangesetComment( 5 | changesetId: number, 6 | commentText: string, 7 | options?: FetchOptions 8 | ): Promise { 9 | await osmFetch(`/0.6/changeset/${changesetId}/comment`, undefined, { 10 | ...options, 11 | method: "POST", 12 | body: `text=${encodeURIComponent(commentText)}`, 13 | headers: { 14 | ...options?.headers, 15 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", 16 | }, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/api/changesets/getChangesetDiff.ts: -------------------------------------------------------------------------------- 1 | import type { OsmChange } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | import type { RawOsmChange } from "../_rawResponse"; 4 | import { parseOsmChangeJson } from "./_parseOsmChangeXml"; 5 | 6 | /** gets the osmChange file for a changeset */ 7 | export async function getChangesetDiff( 8 | id: number, 9 | options?: FetchOptions 10 | ): Promise { 11 | const raw = await osmFetch( 12 | `/0.6/changeset/${id}/download`, 13 | undefined, 14 | options 15 | ); 16 | 17 | return parseOsmChangeJson(raw); 18 | } 19 | -------------------------------------------------------------------------------- /src/api/changesets/getChangesets.ts: -------------------------------------------------------------------------------- 1 | import type { BBox, Changeset } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | import type { RawChangeset } from "../_rawResponse"; 4 | 5 | const mapRawChangeset = ({ comments, ...raw }: RawChangeset): Changeset => ({ 6 | ...raw, 7 | created_at: new Date(raw.created_at), 8 | closed_at: raw.closed_at ? new Date(raw.closed_at) : undefined!, 9 | 10 | discussion: comments?.map((comment) => ({ 11 | ...comment, 12 | date: new Date(comment.date), 13 | uid: `${comment.uid}`, 14 | })), 15 | }); 16 | 17 | export type ListChangesetOptions = { 18 | /** Find changesets within the given bounding box */ 19 | bbox?: BBox | string; 20 | /** Limits the number of changesets returned @default 100 */ 21 | limit?: number; 22 | /** if specified, only opened or closed changesets will be returned */ 23 | only?: "opened" | "closed"; 24 | /** Find changesets by the user. You cannot supply both `user` and `display_name` */ 25 | user?: number; 26 | /** Find changesets by the user. You cannot supply both `user` and `display_name` */ 27 | display_name?: string; 28 | /** 29 | * You can either: 30 | * - specify a single ISO Date, to find changesets closed after that date 31 | * - or, specify a date range to find changesets that were closed after 32 | * `start` and created before `end`. In other words, any changesets that 33 | * were open at some time during the given time range `start` to `end`. 34 | */ 35 | time?: string | [start: string, end: string]; 36 | /** Finds changesets with the specified ids */ 37 | changesets?: number[]; 38 | }; 39 | 40 | /** 41 | * get a list of changesets based on the query. You must supply one of: 42 | * `bbox`, `user`, `display_name`, or `changesets`. 43 | * 44 | * If multiple queries are given, the result will be those which match 45 | * **all** of the requirements. 46 | * 47 | * Returns at most 100 changesets. 48 | */ 49 | export async function listChangesets( 50 | query: ListChangesetOptions, 51 | options?: FetchOptions 52 | ): Promise { 53 | const { only, ...otherQueries } = query; 54 | 55 | const raw = await osmFetch<{ changesets: RawChangeset[] }>( 56 | "/0.6/changesets.json", 57 | { 58 | ...(only && { [only]: true }), 59 | ...otherQueries, 60 | }, 61 | options 62 | ); 63 | 64 | return raw.changesets.map(mapRawChangeset); 65 | } 66 | 67 | /** get a single changeset */ 68 | export async function getChangeset( 69 | id: number, 70 | // eslint-disable-next-line default-param-last 71 | includeDiscussion = true, 72 | options?: FetchOptions 73 | ): Promise { 74 | const raw = await osmFetch<{ changeset: RawChangeset }>( 75 | `/0.6/changeset/${id}.json`, 76 | includeDiscussion ? { include_discussion: 1 } : {}, 77 | options 78 | ); 79 | 80 | return mapRawChangeset(raw.changeset); 81 | } 82 | -------------------------------------------------------------------------------- /src/api/changesets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chunkOsmChange"; 2 | export * from "./createChangesetComment"; 3 | export * from "./getChangesetDiff"; 4 | export * from "./getChangesets"; 5 | export * from "./subscription"; 6 | export * from "./uploadChangeset"; 7 | export { parseOsmChangeXml } from "./_parseOsmChangeXml"; 8 | export { createOsmChangeXml } from "./_createOsmChangeXml"; 9 | -------------------------------------------------------------------------------- /src/api/changesets/subscription.ts: -------------------------------------------------------------------------------- 1 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 2 | 3 | export async function subscribeToChangeset( 4 | changesetId: number, 5 | options?: FetchOptions 6 | ): Promise { 7 | await osmFetch( 8 | `/0.6/changeset/${changesetId}/subscribe`, 9 | {}, 10 | { ...options, method: "POST" } 11 | ); 12 | } 13 | 14 | export async function unsubscribeFromChangeset( 15 | changesetId: number, 16 | options?: FetchOptions 17 | ): Promise { 18 | await osmFetch( 19 | `/0.6/changeset/${changesetId}/unsubscribe`, 20 | {}, 21 | { ...options, method: "POST" } 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/api/changesets/uploadChangeset.ts: -------------------------------------------------------------------------------- 1 | import type { OsmChange, Tags } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | import { version } from "../../../package.json"; 4 | import { 5 | createChangesetMetaXml, 6 | createOsmChangeXml, 7 | } from "./_createOsmChangeXml"; 8 | import { chunkOsmChange, getOsmChangeSize } from "./chunkOsmChange"; 9 | 10 | export interface UploadChunkInfo { 11 | /** the total number of features being uploaded (counting all chunks) */ 12 | featureCount: number; 13 | 14 | /** the index of this changeset (the first is 0) */ 15 | changesetIndex: number; 16 | /** the number of changesets required for this upload */ 17 | changesetTotal: number; 18 | } 19 | 20 | /** 21 | * uploads a changeset to the OSM API. 22 | * @returns the changeset number 23 | */ 24 | export async function uploadChangeset( 25 | tags: Tags, 26 | diff: OsmChange, 27 | options?: FetchOptions & { 28 | /** 29 | * Some changesets are too big to upload, so they have to be 30 | * split ("chunked") into multiple changesets. 31 | * When this happens, you can customize the changeset tags for 32 | * each chunk by returning {@link Tags}. 33 | */ 34 | onChunk?(info: UploadChunkInfo): Tags; 35 | } 36 | ): Promise { 37 | const chunks = chunkOsmChange(diff); 38 | const csIds: number[] = []; 39 | 40 | const featureCount = getOsmChangeSize(diff); 41 | 42 | for (const [index, chunk] of chunks.entries()) { 43 | let tagsForChunk = tags; 44 | 45 | // if this is a chunk of an enourmous changeset, the tags 46 | // for each chunk get custom tags 47 | if (chunks.length > 1) { 48 | if (options?.onChunk) { 49 | // there is a custom implementation for tags. 50 | tagsForChunk = options.onChunk({ 51 | featureCount, 52 | changesetIndex: index, 53 | changesetTotal: chunks.length, 54 | }); 55 | } else { 56 | // there is no custom implementation, 57 | // so add some default tags to the changeset. 58 | tagsForChunk["chunk"] = `${index + 1}/${chunks.length}`; 59 | } 60 | } 61 | 62 | // if the user didn't include a `created_by` tag, we'll add one. 63 | tagsForChunk["created_by"] ||= `osm-api-js ${version}`; 64 | 65 | const csId = +(await osmFetch("/0.6/changeset/create", undefined, { 66 | ...options, 67 | method: "PUT", 68 | body: createChangesetMetaXml(tagsForChunk), 69 | headers: { 70 | ...options?.headers, 71 | "content-type": "application/xml; charset=utf-8", 72 | }, 73 | })); 74 | 75 | const osmChangeXml = createOsmChangeXml(csId, chunk); 76 | 77 | await osmFetch(`/0.6/changeset/${csId}/upload`, undefined, { 78 | ...options, 79 | method: "POST", 80 | body: osmChangeXml, 81 | headers: { 82 | ...options?.headers, 83 | "content-type": "application/xml; charset=utf-8", 84 | }, 85 | }); 86 | 87 | await osmFetch(`/0.6/changeset/${csId}/close`, undefined, { 88 | ...options, 89 | method: "PUT", 90 | }); 91 | csIds.push(csId); 92 | } 93 | 94 | return csIds[0]; // TODO:(semver breaking) return an array of IDs 95 | } 96 | -------------------------------------------------------------------------------- /src/api/getCapabilities.ts: -------------------------------------------------------------------------------- 1 | import { type FetchOptions, osmFetch } from "./_osmFetch"; 2 | 3 | export type ApiStatus = "online" | "offline"; 4 | 5 | export type ApiCapabilities = { 6 | version: string; 7 | generator: string; 8 | copyright: string; 9 | attribution: string; 10 | license: string; 11 | api: { 12 | version: { 13 | minimum: string; 14 | maximum: string; 15 | }; 16 | area: { 17 | maximum: number; 18 | }; 19 | note_area: { 20 | maximum: number; 21 | }; 22 | tracepoints: { 23 | per_page: number; 24 | }; 25 | waynodes: { 26 | maximum: number; 27 | }; 28 | relationmembers: { 29 | maximum: number; 30 | }; 31 | changesets: { 32 | maximum_elements: number; 33 | default_query_limit: number; 34 | maximum_query_limit: number; 35 | }; 36 | notes: { 37 | default_query_limit: number; 38 | maximum_query_limit: number; 39 | }; 40 | timeout: { 41 | seconds: number; 42 | }; 43 | status: { 44 | database: ApiStatus; 45 | api: ApiStatus; 46 | gpx: ApiStatus; 47 | }; 48 | }; 49 | policy: { 50 | imagery: { 51 | blacklist: { regex: string }[]; 52 | }; 53 | }; 54 | }; 55 | 56 | /** This API provides information about the capabilities and limitations of the current API. */ 57 | export async function getApiCapabilities( 58 | options?: FetchOptions 59 | ): Promise { 60 | return osmFetch("/capabilities.json", undefined, options); 61 | } 62 | 63 | // ---------------------------------------------------------------------- // 64 | 65 | /** @deprecated Use {@link getApiCapabilities} instead */ 66 | export type OsmCapabilities = { 67 | limits: { 68 | maxArea: number; 69 | maxNoteArea: number; 70 | maxTracepointPerPage: number; 71 | maxWayNodes: number; 72 | maxChangesetElements: number; 73 | maxTimeout: number; 74 | }; 75 | policy: { 76 | imageryBlacklist: string[]; 77 | }; 78 | }; 79 | 80 | /** 81 | * @deprecated Use {@link getApiCapabilities} instead. This method was 82 | * created before the API supported JSON, and it does not contain every 83 | * field. 84 | */ 85 | export async function getCapabilities(): Promise { 86 | const raw = await getApiCapabilities(); 87 | 88 | const out: OsmCapabilities = { 89 | limits: { 90 | maxArea: raw.api.area.maximum, 91 | maxNoteArea: raw.api.note_area.maximum, 92 | maxTracepointPerPage: raw.api.tracepoints.per_page, 93 | maxWayNodes: raw.api.waynodes.maximum, 94 | maxChangesetElements: raw.api.changesets.maximum_elements, 95 | maxTimeout: raw.api.timeout.seconds, 96 | }, 97 | policy: { 98 | imageryBlacklist: raw.policy.imagery.blacklist.map((item) => item.regex), 99 | }, 100 | }; 101 | 102 | return out; 103 | } 104 | -------------------------------------------------------------------------------- /src/api/getFeature.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | OsmFeature, 3 | OsmFeatureType, 4 | OsmRelation, 5 | OsmWay, 6 | UtilFeatureForType, 7 | } from "../types"; 8 | import { type FetchOptions, osmFetch } from "./_osmFetch"; 9 | 10 | /** 11 | * Gets infomation about a node, way, or relation. 12 | * 13 | * @param full if true, the members of the relation and the nodes of any ways 14 | * will be returned. This option has no effect for nodes 15 | * 16 | * @returns an array, which will only have one item unless you specify `full=true` 17 | */ 18 | export async function getFeature( 19 | type: T, 20 | id: number, 21 | full?: false, 22 | options?: FetchOptions 23 | ): Promise<[UtilFeatureForType]>; 24 | 25 | /** 26 | * Gets infomation about a node, way, or relation. 27 | * 28 | * @param full if true, the members of the relation and the nodes of any ways 29 | * will be returned. This option has no effect for nodes 30 | * 31 | * @returns an array, which will only have one item unless you specify `full=true` 32 | */ 33 | export async function getFeature( 34 | type: OsmFeatureType, 35 | id: number, 36 | full: true, 37 | options?: FetchOptions 38 | ): Promise; 39 | 40 | export async function getFeature( 41 | type: OsmFeatureType, 42 | id: number, 43 | full?: boolean, 44 | options?: FetchOptions 45 | ): Promise { 46 | const suffix = full && type !== "node" ? "/full" : ""; 47 | const raw = await osmFetch<{ elements: OsmFeature[] }>( 48 | `/0.6/${type}/${id}${suffix}.json`, 49 | undefined, 50 | options 51 | ); 52 | 53 | return raw.elements; 54 | } 55 | 56 | /** 57 | * Gets infomation about **multiple** nodes, ways, or relations. 58 | * The IDs can be numbers, or a number plus a version (e.g. `123456789v2`) 59 | */ 60 | export async function getFeatures( 61 | type: T, 62 | ids: (number | string)[], 63 | options?: FetchOptions 64 | ): Promise[]> { 65 | const raw = await osmFetch<{ elements: UtilFeatureForType[] }>( 66 | `/0.6/${type}s.json?${type}s=${ids.join(",")}`, 67 | undefined, 68 | options 69 | ); 70 | return raw.elements; 71 | } 72 | 73 | /** 74 | * Similar to `getFeature()`, except that this fetched **the specified version** 75 | * of the node, way, or relation. 76 | * 77 | * Unlike `getFeature()`, the `full` option is not supported. 78 | */ 79 | export async function getFeatureAtVersion( 80 | type: T, 81 | id: number, 82 | version: number, 83 | options?: FetchOptions 84 | ): Promise> { 85 | const raw = await osmFetch<{ elements: [UtilFeatureForType] }>( 86 | `/0.6/${type}/${id}/${version}.json`, 87 | undefined, 88 | options 89 | ); 90 | 91 | return raw.elements[0]; 92 | } 93 | 94 | /** 95 | * Similar as `getFeature()`, except that this fetches **all versions** 96 | * of the node, way, or relation. 97 | * Unlike `getFeature()`, the `full` option is not supported. 98 | */ 99 | export async function getFeatureHistory( 100 | type: T, 101 | id: number, 102 | options?: FetchOptions 103 | ): Promise[]> { 104 | const raw = await osmFetch<{ elements: UtilFeatureForType[] }>( 105 | `/0.6/${type}/${id}/history.json`, 106 | undefined, 107 | options 108 | ); 109 | 110 | return raw.elements; 111 | } 112 | 113 | /** gets a list of ways that a node belongs to */ 114 | export async function getWaysForNode( 115 | nodeId: number, 116 | options?: FetchOptions 117 | ): Promise { 118 | const raw = await osmFetch<{ elements: OsmWay[] }>( 119 | `/0.6/node/${nodeId}/ways.json`, 120 | undefined, 121 | options 122 | ); 123 | return raw.elements; 124 | } 125 | 126 | /** gets a list of relations that a node, way, or relation belongs to */ 127 | export async function getRelationsForElement( 128 | type: OsmFeatureType, 129 | id: number, 130 | options?: FetchOptions 131 | ): Promise { 132 | const raw = await osmFetch<{ elements: OsmRelation[] }>( 133 | `/0.6/${type}/${id}/relations.json`, 134 | undefined, 135 | options 136 | ); 137 | return raw.elements; 138 | } 139 | -------------------------------------------------------------------------------- /src/api/getMapData.ts: -------------------------------------------------------------------------------- 1 | import type { BBox, OsmFeature } from "../types"; 2 | import { type FetchOptions, osmFetch } from "./_osmFetch"; 3 | 4 | /** 5 | * This command returns: 6 | * - All nodes that are inside the given bounding box and any relations that reference them. 7 | * - All ways that reference at least one node that is inside the given bounding box, any relations that reference those ways, and any nodes outside the bounding box that those ways may reference. 8 | * - All relations that reference one of the nodes, ways or relations included due to the above rules. (Does not apply recursively) 9 | */ 10 | export async function getMapData( 11 | bbox: BBox | string, 12 | options?: FetchOptions 13 | ): Promise { 14 | const raw = await osmFetch<{ elements: OsmFeature[] }>( 15 | "/0.6/map.json", 16 | { bbox }, 17 | options 18 | ); 19 | 20 | return raw.elements; 21 | } 22 | -------------------------------------------------------------------------------- /src/api/getPermissions.ts: -------------------------------------------------------------------------------- 1 | import type { OsmOAuth2Scopes } from "../auth/types"; 2 | import { type FetchOptions, osmFetch } from "./_osmFetch"; 3 | 4 | export type Permissions = { 5 | permissions: `allow_${OsmOAuth2Scopes}`[]; 6 | }; 7 | 8 | /** Gets the OAuth scopes that this app has access to. */ 9 | export async function getPermissions( 10 | options?: FetchOptions 11 | ): Promise { 12 | return osmFetch("/0.6/permissions.json", undefined, options); 13 | } 14 | -------------------------------------------------------------------------------- /src/api/getUIdFromDisplayName.ts: -------------------------------------------------------------------------------- 1 | import { listChangesets } from "./changesets"; 2 | 3 | export async function getUIdFromDisplayName( 4 | displayName: string 5 | ): Promise { 6 | // this is inefficient, but the only way of doing it 7 | const cs = await listChangesets({ display_name: displayName }); 8 | 9 | if (!cs.length) { 10 | throw new Error( 11 | "Could not get uid because the user has never edited the map" 12 | ); 13 | } 14 | 15 | return cs[0].uid; 16 | } 17 | -------------------------------------------------------------------------------- /src/api/getUser.ts: -------------------------------------------------------------------------------- 1 | import type { OsmOwnUser, OsmUser, OsmUserBlock } from "../types"; 2 | import { type FetchOptions, osmFetch } from "./_osmFetch"; 3 | 4 | export async function getUser( 5 | user: number, 6 | options?: FetchOptions 7 | ): Promise; 8 | export async function getUser( 9 | user: "me", 10 | options?: FetchOptions 11 | ): Promise; 12 | 13 | export async function getUser( 14 | user: number | "me", 15 | options?: FetchOptions 16 | ): Promise { 17 | const raw = await osmFetch<{ user: OsmUser | OsmOwnUser }>( 18 | `/0.6/user/${user === "me" ? "details" : user}.json`, 19 | undefined, 20 | options 21 | ); 22 | 23 | return { 24 | ...raw.user, 25 | account_created: new Date(raw.user.account_created), 26 | }; 27 | } 28 | 29 | export async function getUsers( 30 | users: number[], 31 | options?: FetchOptions 32 | ): Promise { 33 | const raw = await osmFetch<{ users: OsmUser[] }>( 34 | "/0.6/users", 35 | { users }, 36 | options 37 | ); 38 | 39 | return raw.users.map((u) => ({ 40 | ...u, 41 | account_created: new Date(u.account_created), 42 | })); 43 | } 44 | 45 | /** gets details about a DWG block given the ID of the block */ 46 | export async function getUserBlockById( 47 | blockId: number, 48 | options?: FetchOptions 49 | ): Promise { 50 | const raw = await osmFetch<{ user_block: OsmUserBlock }>( 51 | `/0.6/user_blocks/${blockId}.json`, 52 | undefined, 53 | options 54 | ); 55 | 56 | return raw.user_block; 57 | } 58 | 59 | /** lists any blocks that are currently active for the authenticated user */ 60 | export async function getOwnUserBlocks( 61 | options?: FetchOptions 62 | ): Promise { 63 | const raw = await osmFetch<{ user_blocks: OsmUserBlock[] }>( 64 | "/0.6/user/blocks/active", 65 | undefined, 66 | options 67 | ); 68 | 69 | return raw.user_blocks; 70 | } 71 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./changesets"; 2 | export * from "./messages"; 3 | export * from "./notes"; 4 | export * from "./preferences"; 5 | 6 | export * from "./getCapabilities"; 7 | export * from "./getFeature"; 8 | export * from "./getMapData"; 9 | export * from "./getPermissions"; 10 | export * from "./getUIdFromDisplayName"; 11 | export * from "./getUser"; 12 | -------------------------------------------------------------------------------- /src/api/messages/deleteMessage.ts: -------------------------------------------------------------------------------- 1 | import type { OsmMessage } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | 4 | export async function deleteMessage( 5 | messageId: number, 6 | options?: FetchOptions 7 | ): Promise { 8 | const raw = await osmFetch<{ message: OsmMessage }>( 9 | `/0.6/user/messages/${messageId}.json`, 10 | {}, 11 | { ...options, method: "DELETE" } 12 | ); 13 | 14 | return raw.message; 15 | } 16 | -------------------------------------------------------------------------------- /src/api/messages/getMessage.ts: -------------------------------------------------------------------------------- 1 | import type { OsmMessage } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | 4 | export async function getMessage( 5 | messageId: number, 6 | options?: FetchOptions 7 | ): Promise { 8 | const raw = await osmFetch<{ message: OsmMessage }>( 9 | `/0.6/user/messages/${messageId}.json`, 10 | undefined, 11 | options 12 | ); 13 | 14 | return raw.message; 15 | } 16 | -------------------------------------------------------------------------------- /src/api/messages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deleteMessage"; 2 | export * from "./getMessage"; 3 | export * from "./listMessages"; 4 | export * from "./sendMessage"; 5 | export * from "./updateMessageReadStatus"; 6 | -------------------------------------------------------------------------------- /src/api/messages/listMessages.ts: -------------------------------------------------------------------------------- 1 | import type { OsmMessageWithoutBody } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | 4 | export async function listMessages( 5 | type: "inbox" | "outbox", 6 | options?: FetchOptions 7 | ): Promise { 8 | const raw = await osmFetch<{ messages: OsmMessageWithoutBody[] }>( 9 | `/0.6/user/messages/${type}.json`, 10 | undefined, 11 | options 12 | ); 13 | 14 | return raw.messages; 15 | } 16 | -------------------------------------------------------------------------------- /src/api/messages/sendMessage.ts: -------------------------------------------------------------------------------- 1 | import type { OsmMessage } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | 4 | export interface SendMessageOptions { 5 | /** Recipient user ID. Specify either `recipient` or `recipient_id`. */ 6 | recipient_id?: number; 7 | /** Recipient's display name. Specify either `recipient` or `recipient_id`. */ 8 | recipient?: string; 9 | /** The title (subject) of the message. */ 10 | title: string; 11 | /** Full content and metadata of the updated message in XML or JSON format. */ 12 | body: string; 13 | /** Format of the body message @default markdown */ 14 | body_format?: OsmMessage["body_format"]; 15 | } 16 | 17 | export async function sendMessage( 18 | message: SendMessageOptions, 19 | options?: FetchOptions 20 | ): Promise { 21 | const raw = await osmFetch<{ message: OsmMessage }>( 22 | "/0.6/user/messages.json", 23 | message, 24 | { ...options, method: "POST" } 25 | ); 26 | 27 | return raw.message; 28 | } 29 | -------------------------------------------------------------------------------- /src/api/messages/updateMessageReadStatus.ts: -------------------------------------------------------------------------------- 1 | import type { OsmMessage } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | 4 | export async function updateMessageReadStatus( 5 | messageId: number, 6 | isRead: boolean, 7 | options?: FetchOptions 8 | ): Promise { 9 | const raw = await osmFetch<{ message: OsmMessage }>( 10 | `/0.6/user/messages/${messageId}.json`, 11 | { read_status: isRead }, 12 | { ...options, method: "POST" } 13 | ); 14 | 15 | return raw.message; 16 | } 17 | -------------------------------------------------------------------------------- /src/api/notes/getNotes.ts: -------------------------------------------------------------------------------- 1 | import type { BBox, OsmNote } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | import type { RawNotesSearch } from "../_rawResponse"; 4 | 5 | const featureToNote = (feature: RawNotesSearch["features"][0]): OsmNote => { 6 | const [lng, lat] = feature.geometry.coordinates; 7 | return { 8 | ...feature.properties, 9 | location: { lat, lng }, 10 | }; 11 | }; 12 | 13 | export type ListNotesOptions = { 14 | /** The search query */ 15 | q: string; 16 | /** Limits notes to the given bounding box */ 17 | bbox?: BBox | string; 18 | /** 19 | * The number of entries returned at max. 20 | * @default 100 21 | */ 22 | limit?: number; 23 | /** 24 | * The number of days a note needs to be closed to no longer be returned. 25 | * @default 7 26 | */ 27 | closed?: number; 28 | /** 29 | * The creator of the returned notes by the display name. 30 | * Does not work together with the user parameter. 31 | */ 32 | display_name?: string; 33 | /** 34 | * The creator of the returned notes by the id of the user. 35 | * Does not work together with the display_name parameter. 36 | */ 37 | user?: number; 38 | /** The beginning of a date range to search in for a note */ 39 | from?: string; 40 | /** The end of a date range to search in for a note */ 41 | to?: string; 42 | /** The value which should be used to sort the notes */ 43 | sort?: "created_at" | "updated_at"; 44 | /** The order of the returned notes */ 45 | order?: "oldest" | "newest"; 46 | }; 47 | 48 | async function $getNotes( 49 | query: ListNotesOptions | { bbox: string | BBox }, 50 | suffix: boolean, 51 | options: FetchOptions | undefined 52 | ): Promise { 53 | const raw = await osmFetch( 54 | `/0.6/notes${suffix ? "/search" : ""}.json`, 55 | query, 56 | options 57 | ); 58 | 59 | return raw.features.map(featureToNote); 60 | } 61 | 62 | /** 63 | * Returns a list of notes matching either the initial note text, or any of the 64 | * comments. The notes will be ordered by the date of their last change, with 65 | * the most recent one first. 66 | * 67 | * If no query is specified, the latest notes are returned. 68 | */ 69 | export function getNotesForQuery( 70 | query: ListNotesOptions, 71 | options?: FetchOptions 72 | ): Promise { 73 | return $getNotes(query, true, options); 74 | } 75 | 76 | /** 77 | * Returns a list of notes within the specified bounding box. The notes 78 | * will be ordered by the date of their last change, with the most recent 79 | * one first. 80 | */ 81 | export function getNotesForArea( 82 | bbox: BBox | string, 83 | options?: FetchOptions 84 | ): Promise { 85 | return $getNotes({ bbox }, false, options); 86 | } 87 | 88 | export async function getNote( 89 | noteId: number, 90 | options?: FetchOptions 91 | ): Promise { 92 | const raw = await osmFetch( 93 | `/0.6/notes/${noteId}.json`, 94 | undefined, 95 | options 96 | ); 97 | return featureToNote(raw); 98 | } 99 | -------------------------------------------------------------------------------- /src/api/notes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getNotes"; 2 | export * from "./noteActions"; 3 | export * from "./subscription"; 4 | -------------------------------------------------------------------------------- /src/api/notes/noteActions.ts: -------------------------------------------------------------------------------- 1 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 2 | 3 | export async function createNote( 4 | lat: number, 5 | lng: number, 6 | text: string, 7 | options?: FetchOptions 8 | ): Promise { 9 | await osmFetch("/0.6/notes", { lat, lon: lng, text }, options); 10 | } 11 | 12 | export async function commentOnNote( 13 | nodeId: number, 14 | text: string, 15 | options?: FetchOptions 16 | ): Promise { 17 | await osmFetch(`/0.6/notes/${nodeId}/comment`, { text }, options); 18 | } 19 | 20 | export async function reopenNote( 21 | nodeId: number, 22 | text?: string, 23 | options?: FetchOptions 24 | ): Promise { 25 | await osmFetch(`/0.6/notes/${nodeId}/reopen`, { text }, options); 26 | } 27 | -------------------------------------------------------------------------------- /src/api/notes/subscription.ts: -------------------------------------------------------------------------------- 1 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 2 | 3 | export async function subscribeToNote( 4 | noteId: number, 5 | options?: FetchOptions 6 | ): Promise { 7 | await osmFetch( 8 | `/0.6/notes/${noteId}/subscription`, 9 | {}, 10 | { ...options, method: "POST" } 11 | ); 12 | } 13 | 14 | export async function unsubscribeFromNote( 15 | noteId: number, 16 | options?: FetchOptions 17 | ): Promise { 18 | await osmFetch( 19 | `/0.6/notes/${noteId}/subscription`, 20 | {}, 21 | { ...options, method: "DELETE" } 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/api/preferences/deletePreferences.ts: -------------------------------------------------------------------------------- 1 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 2 | 3 | export async function deletePreferences( 4 | key: string, 5 | options?: FetchOptions 6 | ): Promise { 7 | await osmFetch<"">( 8 | `/0.6/user/preferences/${key}.json`, 9 | {}, 10 | { ...options, method: "DELETE" } 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/api/preferences/getPreferences.ts: -------------------------------------------------------------------------------- 1 | import type { Tags } from "../../types"; 2 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 3 | 4 | export async function getPreferences(options?: FetchOptions): Promise { 5 | const raw = await osmFetch<{ preferences: Tags }>( 6 | "/0.6/user/preferences.json", 7 | undefined, 8 | options 9 | ); 10 | 11 | return raw.preferences; 12 | } 13 | -------------------------------------------------------------------------------- /src/api/preferences/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deletePreferences"; 2 | export * from "./getPreferences"; 3 | export * from "./updatePreferences"; 4 | -------------------------------------------------------------------------------- /src/api/preferences/updatePreferences.ts: -------------------------------------------------------------------------------- 1 | import { type FetchOptions, osmFetch } from "../_osmFetch"; 2 | 3 | export async function updatePreferences( 4 | key: string, 5 | value: string, 6 | options?: FetchOptions 7 | ): Promise { 8 | await osmFetch<"">( 9 | `/0.6/user/preferences/${key}.json`, 10 | {}, 11 | { ...options, method: "PUT", body: value } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/auth/createPopup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * resolves once the login flow in the popup is sucessful. 3 | * rejects if the popup is closed by the user or if there's an error. 4 | * @internal 5 | */ 6 | export function createPopup(loginUrl: string): Promise { 7 | let resolved = false; 8 | return new Promise((resolve, reject) => { 9 | const [width, height] = [600, 550]; 10 | const settings = Object.entries({ 11 | width, 12 | height, 13 | left: window.screen.width / 2 - width / 2, 14 | top: window.screen.height / 2 - height / 2, 15 | }) 16 | .map(([k, v]) => `${k}=${v}`) 17 | .join(","); 18 | 19 | const popup = window.open("about:blank", "oauth_window", settings); 20 | if (!popup) throw new Error("Popup was blocked"); 21 | popup.location = loginUrl; 22 | 23 | window.authComplete = (fullUrl: string) => { 24 | resolve(fullUrl); 25 | resolved = true; 26 | }; 27 | 28 | // check every 0.5seconds if the popup has been closed by the user. 29 | const intervalId = setInterval(() => { 30 | if (popup.closed) { 31 | if (!resolved) reject(new Error("Cancelled")); 32 | clearInterval(intervalId); 33 | } 34 | }, 500); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/auth/exchangeCode.ts: -------------------------------------------------------------------------------- 1 | import { getOAuthBaseUrl } from "./helpers"; 2 | import type { LoginData, OsmOAuth2Scopes, Transaction } from "./types"; 3 | 4 | type ExchangeRawResponse = 5 | | { 6 | access_token: string; 7 | created_at: number; 8 | scope: string; 9 | token_type: "Bearer"; 10 | } 11 | | { error_description: string }; 12 | 13 | /** @interal */ 14 | export async function exchangeCode( 15 | fullUrl: string, 16 | { options, pkceVerifier, state }: Transaction 17 | ): Promise { 18 | const responseQs = new URL(fullUrl).searchParams; 19 | const error = responseQs.get("error_description"); 20 | const code = responseQs.get("code"); 21 | const responseState = responseQs.get("state"); 22 | 23 | if (error) throw new Error(error); 24 | if (!code) throw new Error("No code in OAuth response"); 25 | if (!pkceVerifier || !state) throw new Error("No login in progress"); 26 | if (state !== responseState) throw new Error("State Mismatch"); 27 | 28 | const qs = { 29 | grant_type: "authorization_code", 30 | code, 31 | redirect_uri: options.redirectUrl, 32 | client_id: options.clientId, 33 | code_verifier: pkceVerifier, 34 | }; 35 | const url = `${getOAuthBaseUrl()}/oauth2/token?${new URLSearchParams( 36 | qs 37 | ).toString()}`; 38 | const request = await fetch(url, { 39 | method: "POST", 40 | body: "", 41 | headers: { 42 | "Content-Type": "application/x-www-form-urlencoded", 43 | }, 44 | }); 45 | const exchangeResponse: ExchangeRawResponse = await request.json(); 46 | 47 | if ("error_description" in exchangeResponse) { 48 | throw new Error(exchangeResponse.error_description); 49 | } 50 | 51 | const loginData: LoginData = { 52 | issuedAt: new Date(exchangeResponse.created_at * 1000).toISOString(), 53 | accessToken: exchangeResponse.access_token, 54 | scopes: exchangeResponse.scope.split(" ") as OsmOAuth2Scopes[], 55 | }; 56 | localStorage.setItem("__osmAuth", JSON.stringify(loginData)); 57 | 58 | // At this point, we can consider the login sucessfull 59 | delete window.authComplete; 60 | 61 | return loginData; 62 | } 63 | -------------------------------------------------------------------------------- /src/auth/helpers.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../config"; 2 | 3 | /** @internal return a sha256 hash, encoded using base64-url */ 4 | export async function sha256(text: string) { 5 | const data = new TextEncoder().encode(text); 6 | const buf = await window.crypto.subtle.digest("SHA-256", data); 7 | 8 | // eslint-disable-next-line unicorn/prefer-code-point -- not sure if this autofix is safe 9 | const base64 = btoa(String.fromCharCode(...new Uint8Array(buf))); 10 | 11 | return base64.replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", ""); 12 | } 13 | 14 | /** @internal */ 15 | export function getRandomString() { 16 | return [...window.crypto.getRandomValues(new Uint32Array(32))] 17 | .map((m) => `0${m.toString(16)}`.slice(-2)) 18 | .join(""); 19 | } 20 | 21 | /** @internal */ 22 | export function getOAuthBaseUrl() { 23 | let baseUrl = getConfig().apiUrl; 24 | 25 | // special step for the main instance of OSM... 26 | if (baseUrl === "https://api.openstreetmap.org") { 27 | baseUrl = "https://www.openstreetmap.org"; 28 | } 29 | return baseUrl; 30 | } 31 | -------------------------------------------------------------------------------- /src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./oauth2"; 2 | -------------------------------------------------------------------------------- /src/auth/oauth2.ts: -------------------------------------------------------------------------------- 1 | import { createPopup } from "./createPopup"; 2 | import { exchangeCode } from "./exchangeCode"; 3 | import { getOAuthBaseUrl, getRandomString, sha256 } from "./helpers"; 4 | import type { LoginData, LoginOptions, Transaction } from "./types"; 5 | 6 | /** 7 | * If mode = `redirect`, this function will never resolve, and will 8 | * redirect to openstreetmap.org instead. 9 | * 10 | * If mode = `popup`, this function will resolve once the popup is 11 | * succesfully closed. It may reject if an error occurs durnig login. 12 | */ 13 | export async function login(options: LoginOptions): Promise { 14 | if (!options.redirectUrl) { 15 | throw new Error("You must include the 'redirectUrl' option"); 16 | } 17 | if (!options.clientId) { 18 | throw new Error("You must include the 'clientId' option"); 19 | } 20 | if (!options.scopes) { 21 | throw new Error("You must include the 'scopes' option"); 22 | } 23 | 24 | const state = getRandomString(); 25 | const pkceVerifier = getRandomString(); 26 | const pkceChallenge = await sha256(pkceVerifier); 27 | 28 | const qs = { 29 | scope: options.scopes.join(" "), 30 | include_granted_scopes: "true", 31 | response_type: "code", 32 | state, 33 | redirect_uri: options.redirectUrl, 34 | client_id: options.clientId, 35 | code_challenge_method: "S256", 36 | code_challenge: pkceChallenge, 37 | }; 38 | 39 | const oauthPath = `/oauth2/authorize?${new URLSearchParams(qs).toString()}`; 40 | 41 | // if switching users, there are two extra steps before the oauth flow starts 42 | const path = options.switchUser 43 | ? `/logout?referer=${encodeURIComponent( 44 | `/login?referer=${encodeURIComponent(oauthPath)}` 45 | )}` 46 | : oauthPath; 47 | 48 | const loginUrl = getOAuthBaseUrl() + path; 49 | 50 | const transaction: Transaction = { state, pkceVerifier, options }; 51 | 52 | if (options.mode === "popup") { 53 | const fullUrl = await createPopup(loginUrl); 54 | return exchangeCode(fullUrl, transaction); 55 | } 56 | 57 | if (options.mode === "redirect") { 58 | localStorage.setItem("__osmAuthTemp", JSON.stringify(transaction)); 59 | window.location.replace(loginUrl); 60 | return undefined as never; 61 | } 62 | 63 | throw new Error("options.mode must be 'popup' or 'redirect'"); 64 | } 65 | 66 | /** 67 | * if you used the `redirect` method to login, you need to await this variable 68 | * before you can safely determine if a user is logged in or not. 69 | */ 70 | export const authReady: Promise = (async () => { 71 | if (typeof window === "undefined") return; // running in nodejs 72 | const fullUrl = window.location.href; 73 | const loginState = localStorage.getItem("__osmAuthTemp"); 74 | 75 | if (new URL(fullUrl).searchParams.get("code")) { 76 | if (window.opener?.authComplete) { 77 | window.opener.authComplete(fullUrl); 78 | window.close(); 79 | } else if (loginState) { 80 | try { 81 | const transaction: Transaction = JSON.parse(loginState); 82 | await exchangeCode(fullUrl, transaction); 83 | localStorage.removeItem("__osmAuthTemp"); 84 | } catch (ex) { 85 | console.error("OSM Auth Error", ex); 86 | } 87 | } else { 88 | // there is ?code= in the URL, but there is no login in progress... 89 | } 90 | } 91 | })(); 92 | 93 | /** returns the OAuth2 `access_token` if the user is logged in, otherwise it returns `undefined` */ 94 | export const getAuthToken = (): string | undefined => { 95 | try { 96 | const maybeJson = localStorage.getItem("__osmAuth"); 97 | return maybeJson 98 | ? (JSON.parse(maybeJson) as LoginData).accessToken 99 | : undefined; 100 | } catch { 101 | return undefined; 102 | } 103 | }; 104 | 105 | /** returns `true` if the user is logged in */ 106 | export const isLoggedIn = (): boolean => !!getAuthToken(); 107 | 108 | export function logout() { 109 | localStorage.removeItem("__osmAuth"); 110 | } 111 | -------------------------------------------------------------------------------- /src/auth/types.ts: -------------------------------------------------------------------------------- 1 | export type OsmOAuth2Scopes = 2 | | "consume_messages" 3 | | "send_messages" 4 | | "read_prefs" 5 | | "write_prefs" 6 | | "write_diary" 7 | | "write_api" 8 | | "write_redactions" 9 | | "read_gpx" 10 | | "write_gpx" 11 | | "write_notes"; 12 | 13 | export type LoginOptions = { 14 | mode: "redirect" | "popup"; 15 | redirectUrl: string; 16 | clientId: string; 17 | scopes: readonly OsmOAuth2Scopes[]; 18 | popupSize?: readonly [width: number, height: number]; 19 | /** 20 | * @default false 21 | * If `true`, the login popup/page will: 22 | * 1. first ask the user to logout of OSM 23 | * 2. Then ask the user to log back in 24 | * 3. Then start the OAuth flow 25 | */ 26 | switchUser?: boolean; 27 | }; 28 | 29 | /** this is the payload that gets stored in localStorage */ 30 | export type LoginData = { 31 | /** ISO Date */ 32 | issuedAt: string; 33 | accessToken: string; 34 | scopes: OsmOAuth2Scopes[]; 35 | }; 36 | 37 | /** @internal */ 38 | export type Transaction = { 39 | state: string; 40 | pkceVerifier: string; 41 | options: LoginOptions; 42 | }; 43 | 44 | /** @internal */ 45 | declare global { 46 | interface Window { 47 | authComplete?(fullUrl: string): void; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | type Config = { 2 | /** the base URL for the OSM API. Defaults to `https://api.openstreetmap.org` */ 3 | apiUrl: string; 4 | /** the HTTP User-Agent sent with every API request */ 5 | userAgent: string; 6 | 7 | /** If you use another library for authentication, you can pass in a custom value for the `Authorization` HTTP header */ 8 | authHeader?: string; 9 | /** Credentials if you want to login with basic auth (**strongly discouraged**) */ 10 | basicAuth?: { username: string; password: string }; 11 | }; 12 | 13 | const config: Config = { 14 | apiUrl: "https://api.openstreetmap.org", 15 | userAgent: "https://github.com/osmlab/osm-api-js", 16 | }; 17 | 18 | export const getConfig = (): Config => config; 19 | 20 | export function configure(updatedConfig: Partial): void { 21 | Object.assign(config, updatedConfig); 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./auth"; 3 | export * from "./config"; 4 | export * from "./types"; 5 | -------------------------------------------------------------------------------- /src/types/changesets.ts: -------------------------------------------------------------------------------- 1 | import type { OsmFeature } from "./features"; 2 | import type { Tags } from "./general"; 3 | 4 | export type ChangesetComment = { 5 | id: number; 6 | visible: boolean; 7 | user: string; 8 | // TODO:(semver breaking) change to number 9 | uid: string; 10 | // TODO:(semver breaking) change to string 11 | date: Date; 12 | text: string; 13 | }; 14 | 15 | export type Changeset = { 16 | id: number; 17 | // TODO:(semver breaking) change to string 18 | created_at: Date; 19 | open: boolean; 20 | comments_count: number; 21 | changes_count: number; 22 | /** property only exists if `open=false` */ 23 | // TODO:(semver breaking) mark as optional 24 | closed_at: Date; 25 | /** property only exists if `open=false` */ 26 | // TODO:(semver breaking) mark as optional 27 | min_lat: number; 28 | /** property only exists if `open=false` */ 29 | // TODO:(semver breaking) mark as optional 30 | min_lon: number; 31 | /** property only exists if `open=false` */ 32 | // TODO:(semver breaking) mark as optional 33 | max_lat: number; 34 | /** property only exists if `open=false` */ 35 | // TODO:(semver breaking) mark as optional 36 | max_lon: number; 37 | uid: number; 38 | user: string; 39 | tags: Tags; 40 | /** the `discussion` attribute is only included in the `getChangeset` API */ 41 | discussion?: ChangesetComment[]; 42 | }; 43 | 44 | export type OsmChange = { 45 | create: OsmFeature[]; 46 | modify: OsmFeature[]; 47 | delete: OsmFeature[]; 48 | }; 49 | -------------------------------------------------------------------------------- /src/types/features.ts: -------------------------------------------------------------------------------- 1 | import type { Tags } from "./general"; 2 | 3 | export type OsmFeatureType = "node" | "way" | "relation"; 4 | 5 | /** these attributes exist on nodes, ways, and relations */ 6 | export type OsmBaseFeature = { 7 | type: string; 8 | changeset: number; 9 | id: number; 10 | 11 | timestamp: string; 12 | user: string; 13 | version: number; 14 | uid: number; 15 | 16 | tags?: Tags; 17 | 18 | /** if false, it means the feature has been deleted */ 19 | visible?: false; 20 | }; 21 | 22 | export type OsmNode = OsmBaseFeature & { 23 | type: "node"; 24 | lat: number; 25 | lon: number; 26 | }; 27 | 28 | export type OsmWay = OsmBaseFeature & { 29 | type: "way"; 30 | nodes: number[]; 31 | }; 32 | 33 | export type OsmRelation = OsmBaseFeature & { 34 | type: "relation"; 35 | members: { type: OsmFeatureType; ref: number; role: string }[]; 36 | }; 37 | 38 | export type OsmFeature = OsmNode | OsmWay | OsmRelation; 39 | 40 | /** utility to get the type of the feature from its name */ 41 | export type UtilFeatureForType = T extends "node" 42 | ? OsmNode 43 | : T extends "way" 44 | ? OsmWay 45 | : T extends "relation" 46 | ? OsmRelation 47 | : never; 48 | -------------------------------------------------------------------------------- /src/types/general.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace OsmApi { 3 | /** 4 | * use this interface to get additional typesafety, see the documentation 5 | * on {@link Key} for more info. 6 | */ 7 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- intentional 8 | interface Keys {} 9 | } 10 | } 11 | 12 | /** 13 | * By default, this library defines {@link Tags} to be `Record`. This 14 | * means that you don't get any typesafety for OSM keys/tags. 15 | * 16 | * There are two methods to make this more strict: 17 | * 18 | * 1. enable TypeScript's `noPropertyAccessFromIndexSignature` along with eslint's `dot-notation`. 19 | * This is not perfect, but it will force you to use `tags[KEY]` instead of `tags.key` which 20 | * makes the hardcoded keys more visible. 21 | * 22 | * 2. Declare a string union for every permitted osm key. For example: 23 | * ```ts 24 | * declare global { 25 | * namespace OsmApi { 26 | * interface Keys { 27 | * keys: 'amenity' | 'highway'; 28 | * } 29 | * } 30 | * } 31 | * export {}; 32 | * ``` 33 | * 34 | * Regardless of what method you use, it also makes sense to enable TypeScript's 35 | * `noUncheckedIndexedAccess` option. 36 | */ 37 | export type Key = OsmApi.Keys extends { keys: string } 38 | ? OsmApi.Keys["keys"] 39 | : string; 40 | 41 | export type Tags = OsmApi.Keys extends { keys: string } 42 | ? Partial> 43 | : Record; 44 | 45 | export type BBox = readonly [ 46 | minLng: number, 47 | minLat: number, 48 | maxLng: number, 49 | maxLat: number, 50 | ]; 51 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./changesets"; 2 | export * from "./features"; 3 | export * from "./general"; 4 | export * from "./messages"; 5 | export * from "./notes"; 6 | export * from "./osmPatch"; 7 | export * from "./user"; 8 | -------------------------------------------------------------------------------- /src/types/messages.ts: -------------------------------------------------------------------------------- 1 | export interface OsmMessage { 2 | id: number; 3 | from_user_id: number; 4 | from_display_name: string; 5 | to_user_id: number; 6 | to_display_name: string; 7 | title: string; 8 | /** ISO Date */ 9 | sent_on: string; 10 | message_read: boolean; 11 | deleted: boolean; 12 | body_format: "text" | "markdown" | "html"; 13 | body: string; 14 | } 15 | 16 | export type OsmMessageWithoutBody = Omit; 17 | -------------------------------------------------------------------------------- /src/types/notes.ts: -------------------------------------------------------------------------------- 1 | export type OsmNoteComment = { 2 | date: string; 3 | uid: number; 4 | user: string; 5 | user_url: string; 6 | action: "opened" | "closed" | "commented" | "reopened"; 7 | text: string; 8 | html: string; 9 | }; 10 | 11 | export type OsmNote = { 12 | location: { lat: number; lng: number }; 13 | id: number; 14 | status: "open" | "closed"; 15 | date_created: string; 16 | comments: OsmNoteComment[]; 17 | url: string; 18 | comment_url: string; 19 | close_url: string; 20 | }; 21 | -------------------------------------------------------------------------------- /src/types/osmPatch.ts: -------------------------------------------------------------------------------- 1 | import type { Feature, Geometry } from "geojson"; 2 | import type { OsmFeatureType } from "./features"; 3 | import type { Tags } from "./general"; 4 | 5 | /** 6 | * An OsmPatchFeature is a GeoJson feature that is part 7 | * of an {@link OsmPatch} file. 8 | */ 9 | export type OsmPatchFeature = Feature< 10 | Geometry, 11 | Tags & { 12 | __action?: "edit" | "move" | "delete"; 13 | __members?: { type: OsmFeatureType; ref: number; role: string }[]; 14 | } 15 | >; 16 | 17 | /** 18 | * An OsmPatch file is a GeoJson file with a few extra properties. 19 | * See https://github.com/osm-nz/linz-address-import/blob/main/SPEC.md 20 | * for more information. 21 | */ 22 | export type OsmPatch = { 23 | type: "FeatureCollection"; 24 | features: OsmPatchFeature[]; 25 | __comment?: string; 26 | size?: "small" | "medium" | "large"; 27 | instructions?: string; 28 | changesetTags?: Tags; 29 | }; 30 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | export type OsmUserRole = "moderator"; 2 | 3 | export type OsmUser = { 4 | id: number; 5 | display_name: string; 6 | account_created: Date; 7 | description: string; 8 | contributor_terms: { agreed: boolean; pd: boolean }; 9 | /** may be undefined if the user has no profile photo */ 10 | img?: { href: string }; 11 | roles: OsmUserRole[]; 12 | changesets: { count: number }; 13 | traces: { count: number }; 14 | blocks: { received: { count: number; active: number } }; 15 | }; 16 | 17 | export type OsmOwnUser = OsmUser & { 18 | home: { lat: number; lon: number; zoom: number }; 19 | languages: string[]; 20 | messages: { 21 | received: { count: number; unread: number }; 22 | sent: { count: number }; 23 | }; 24 | }; 25 | 26 | export interface OsmUserBlock { 27 | id: number; 28 | /** ISO Date */ 29 | created_at: string; 30 | /** ISO Date */ 31 | updated_at: string; 32 | /** ISO Date */ 33 | ends_at: string; 34 | needs_view: boolean; 35 | user: { uid: number; user: string }; 36 | creator: { uid: number; user: string }; 37 | /** field only exists if the block has already been revoked */ 38 | revoker?: { uid: number; user: string }; 39 | reason: string; 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "commonjs", 6 | "target": "es2015", 7 | "lib": ["esnext", "dom"], 8 | "moduleResolution": "node", 9 | "downlevelIteration": true, 10 | "declaration": true, 11 | "stripInternal": true, 12 | "skipLibCheck": true, 13 | "resolveJsonModule": true, 14 | "forceConsistentCasingInFileNames": false, 15 | "noPropertyAccessFromIndexSignature": true, 16 | "outDir": "dist" 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------