diff --git a/.gitignore b/.gitignore index 5479765..e943e33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ env.test.sh dist -node_modules \ No newline at end of file +node_modules + +.env \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f4103f..21ecb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## HEAD +### Bug fixes πŸ› + +- Fix reverse geocoding errors caused by leading/trailing whitespace in coordinate input (e.g. `"48.774989, 9.155557 "`) + ## 5.1.2 ### Bug fixes πŸ› diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..346ffb9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +A vanilla-JS geocoder control (`@mapbox/mapbox-gl-geocoder`) for `mapbox-gl-js`. It wraps the Mapbox Geocoding API (via `@mapbox/mapbox-sdk`) in a `mapboxgl.IControl`-compatible widget, backed by the `suggestions` library for the autocomplete dropdown. + +## Commands + +```bash +npm install && npm start # dev server at http://localhost:9966/ (budo), serves debug/index.js +npm test # pretest runs eslint, then runs tests in Firefox via smokestack +npm run lint # eslint on lib and test +npm run docs # regenerate API.md from lib/index.js JSDoc via `documentation` +npm run prepublish # build dist/mapbox-gl-geocoder.min.js via browserify + babelify + uglify +``` + +- Dev server requires a Mapbox access token in `localStorage`: `localStorage.setItem('MapboxAccessToken', '')`. +- Tests require the `MapboxAccessToken` env var: `export MapboxAccessToken="YOUR ACCESS TOKEN"`. +- There is no single-test-file runner; `npm test` bundles `test/index.js` (which requires `test/test.geocoder.js` and `test/test.ui.js`) with browserify+envify and runs it in Firefox via smokestack. To scope down a run, temporarily comment out one of the `require(...)` lines in `test/index.js`, or use tape's `t.only`/skip individual `test(...)` blocks in the target file. +- Target runtime is browser-only (uses `document`, `window`, `navigator`, `XMLHttpRequest` directly) β€” code must not assume a Node environment despite CommonJS `require`/`module.exports`. + +## Architecture + +- `lib/index.js` β€” `MapboxGeocoder`, the main control. Prototype-based (`MapboxGeocoder.prototype = {...}`, not ES classes). Implements the `mapboxgl.IControl` interface (`onAdd`/`onRemove`) plus a large public getter/setter API (`setProximity`, `setLanguage`, `setFilter`, etc.) mirrored 1:1 by the `options` object passed to the constructor. Also usable standalone via `addTo()` without a map. + - `_geocode()` is the core request pipeline: determines request type (`FORWARD` / `LOCAL` / `REVERSE`, see `GEOCODE_REQUEST_TYPE`) via `_requestType()`, builds the SDK request config via `_setupConfig()`, then merges results from three sources in order β€” Mapbox Geocoding API, `options.localGeocoder` (sync), `options.externalGeocoder` (async/Promise) β€” before applying `options.filter` and updating the `Typeahead` suggestion list. + - `_fly()` handles map animation after a result is selected: uses `exceptions.json`-listed bounding boxes for territories that would otherwise produce absurd fit-bounds (e.g. countries spanning the antimeridian), falls back to the result's `bbox`, then to `flyTo`/`zoom`. + - Reverse geocoding input is detected by `utils.REVERSE_GEOCODE_COORD_RGX` (`lat, lon` or, with `flipCoordinates`, `lon, lat`). + - Geolocation flow (`_geolocateUser`) uses `lib/geolocation.js` to get the browser position, then reverse-geocodes it (unless `localGeocoderOnly`) and renders via `utils.transformFeatureToGeolocationText` with configurable `addressAccuracy`. +- `lib/events.js` β€” `MapboxEventManager`, sends anonymized usage telemetry (`search.start`, `search.keystroke`, `search.select`) to the Mapbox events service, batched/queued and flushed on an interval (`flush`/`push`). Disabled by `options.enableEventLogging = false` or by using a non-default `origin`/`localGeocoder`. Payloads are validated against per-event required-property lists before sending. +- `lib/utils.js` β€” pure helpers: coordinate regex, and featureβ†’text transforms for reverse-geocode display. +- `lib/localization.js` β€” per-language placeholder text lookup, keyed by ISO 639-1 code (via `subtag`). +- `lib/exceptions.js` β€” bbox overrides for territories with problematic auto-fit bounds. +- `lib/mapbox-gl-geocoder.css` β€” control styling, published alongside `dist/`. +- `debug/` β€” manual browser test harness for `npm start` (`index.js`, `filter.js`, `nomap.js`, `mock-api.json`). +- `test/index.js` is the entry point that aggregates `test.geocoder.js` (core control logic, request building, options API) and `test.ui.js` (DOM/interaction behavior); `events.test.js` and `utils.test.js` cover the other two lib modules. All use `tape` + `sinon` for stubbing (e.g. stubbing `geocoderService`/`XMLHttpRequest`) and run against real DOM APIs in a real browser (no jsdom). + +## Conventions + +- Code style is ES5-leaning CommonJS (`var`, prototype objects, `function` expressions) even though some newer syntax (arrow functions, template literals, default params, `const`/`let`) appears in more recently touched code β€” match the surrounding style of whichever function you're editing rather than imposing one style repo-wide. +- Public API additions (new options or methods) should get a matching JSDoc `@param`/method comment block in `lib/index.js`, since `API.md` is generated from it via `npm run docs`. +- ESLint (`.eslintrc`) targets ES6 `parserOptions` but `sourceType: "script"` (no ESM import/export) with 2-space indentation enforced. \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 23eabdb..ec48f27 100644 --- a/lib/index.js +++ b/lib/index.js @@ -784,9 +784,8 @@ MapboxGeocoder.prototype = { case GEOCODE_REQUEST_TYPE.FORWARD: { // Ensure that any reverse geocoding looking request is cleaned up // to be processed as only a forward geocoding request by the server. - const trimmedSearch = search.trim(); const reverseGeocodeCoordRgx = /^(-?\d{1,3}(\.\d{0,256})?)[, ]+(-?\d{1,3}(\.\d{0,256})?)?$/; - if (reverseGeocodeCoordRgx.test(trimmedSearch)) { + if (reverseGeocodeCoordRgx.test(search)) { search = search.replace(/,/g, ' '); } config = extend(config, { query: search }); @@ -799,6 +798,7 @@ MapboxGeocoder.prototype = { }, _geocode: function(searchInput) { + searchInput = searchInput.trim(); this.inputString = searchInput; this._showLoadingIcon(); this._eventEmitter.emit('loading', { query: searchInput }); diff --git a/lib/utils.js b/lib/utils.js index 9c1027c..6db016e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -60,7 +60,7 @@ function getAddressInfo(feature) { return addrInfo; } -const REVERSE_GEOCODE_COORD_RGX = /^[ ]*(-?\d{1,3}(\.\d{0,256})?)[, ]+(-?\d{1,3}(\.\d{0,256})?)[ ]*$/; +const REVERSE_GEOCODE_COORD_RGX = /^(-?\d{1,3}(\.\d{0,256})?)[, ]+(-?\d{1,3}(\.\d{0,256})?)$/; module.exports = { transformFeatureToGeolocationText: transformFeatureToGeolocationText, diff --git a/test/test.geocoder.js b/test/test.geocoder.js index d2c5611..ed65e29 100644 --- a/test/test.geocoder.js +++ b/test/test.geocoder.js @@ -143,6 +143,19 @@ test('geocoder', function(tt) { }); }); + tt.test('forward geocoding - trims surrounding whitespace', function(t) { + t.plan(2); + setup({}); + geocoder.query(' Paris '); + geocoder.on( + 'results', + once(function(e) { + t.equals(geocoder.inputString, 'Paris', 'inputString is trimmed'); + t.equals(e.config.query, 'Paris', 'query sent to API is trimmed'); + }) + ); + }); + tt.test('custom endpoint', function(t) { t.plan(1); setup({ origin: 'localhost:2999' }); @@ -232,6 +245,48 @@ test('geocoder', function(tt) { ); }); + tt.test('options.reverseGeocode - trims surrounding whitespace (trailing space)', function(t) { + t.plan(1); + setup({ + reverseGeocode: true + }); + geocoder.query('48.774989, 9.155557 '); + geocoder.on( + 'results', + once(function(e) { + t.deepEquals(e.query, [ 9.155557, 48.774989 ], 'parses query with no extra/NaN elements'); + }) + ); + }); + + tt.test('options.reverseGeocode - trims surrounding whitespace (leading space)', function(t) { + t.plan(1); + setup({ + reverseGeocode: true + }); + geocoder.query(' 48.774989, 9.155557'); + geocoder.on( + 'results', + once(function(e) { + t.deepEquals(e.query, [ 9.155557, 48.774989 ], 'parses query with no extra/NaN elements'); + }) + ); + }); + + tt.test('options.reverseGeocode - trims whitespace around comma', function(t) { + t.plan(1); + setup({ + reverseGeocode: true + }); + geocoder.query('48.774989 , 9.155557'); + geocoder.on( + 'results', + once(function(e) { + t.deepEquals(e.query, [ 9.155557, 48.774989 ], 'parses query with no extra/NaN elements'); + }) + ); + }); + tt.test('options.reverseGeocode - false by default', function(t) { t.plan(1); setup();