Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ env.test.sh
dist
node_modules

.env
.idea
.env
CLAUDE.local.md
3 changes: 3 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ A geocoder component using the [Mapbox Geocoding API][74]
* `options.enableGeolocation` **[Boolean][80]** If `true` enable user geolocation feature. (optional, default `false`)
* `options.useBrowserFocus` **[Boolean][80]** If `true`, the geocoder will use the browser's focus event to show suggestions. If `false`, it will only highlight active suggestions and Tab will not propagate to the suggestions list. (optional, default `false`)
* `options.addressAccuracy` **(`"address"` | `"street"` | `"place"` | `"country"`)** The accuracy for the geolocation feature with which we define the address line to fill. The browser API returns the user's position with accuracy, and sometimes we can get the neighbor's address. To prevent receiving an incorrect address, you can reduce the accuracy of the definition. (optional, default `"street"`)
* `options.inputTransforms` **[Object][75]?** Options controlling how the search input is transformed before being processed.

* `options.inputTransforms.trimCoordinatesPunctuation` **[Boolean][80]** If `true`, leading/trailing punctuation characters (currently only `;`) are trimmed from the search input. (optional, default `false`)

### Examples

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## HEAD

### Features / Improvements 🚀

- Add `inputTransforms.trimCoordinatesPunctuation` option (defaults to `false`). When enabled, leading/trailing punctuation (e.g. `;`) is trimmed from search input that looks like coordinates (e.g. `"48.774989, 9.155557;"`)

### Bug fixes 🐛

- Fix reverse geocoding errors caused by leading/trailing whitespace in coordinate input (e.g. `"48.774989, 9.155557 "`)
Expand Down
86 changes: 62 additions & 24 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const GEOCODE_REQUEST_TYPE = {
REVERSE: 2,
};

const PUNCTUATION_CHARS = new Set([';']);

/**
* Don't include this as part of the options object when creating a new MapboxGeocoder instance.
*/
Expand Down Expand Up @@ -77,6 +79,8 @@ function getFooterNode() {
* @param {Boolean} [options.enableGeolocation=false] If `true` enable user geolocation feature.
* @param {Boolean} [options.useBrowserFocus=false] If `true`, the geocoder will use the browser's focus event to show suggestions. If `false`, it will only highlight active suggestions and Tab will not propagate to the suggestions list.
* @param {('address'|'street'|'place'|'country')} [options.addressAccuracy="street"] The accuracy for the geolocation feature with which we define the address line to fill. The browser API returns the user's position with accuracy, and sometimes we can get the neighbor's address. To prevent receiving an incorrect address, you can reduce the accuracy of the definition.
* @param {Object} [options.inputTransforms] Options controlling how the search input is transformed before being processed.
* @param {Boolean} [options.inputTransforms.trimCoordinatesPunctuation=false] If `true`, leading/trailing punctuation characters (currently only `;`) are trimmed from the search input.
* @example
* var geocoder = new MapboxGeocoder({ accessToken: mapboxgl.accessToken });
* map.addControl(geocoder);
Expand All @@ -85,26 +89,7 @@ function getFooterNode() {
*/

function MapboxGeocoder(options) {
this._eventEmitter = new EventEmitter();
this.options = extend({}, this.options, options);
this.inputString = '';
this.fresh = true;
this.lastSelected = null;
this.geolocation = new Geolocation();
}

function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

MapboxGeocoder.prototype = {
options: {
const defaultOptions = {
zoom: 16,
flyTo: true,
trackProximity: true,
Expand All @@ -122,17 +107,44 @@ MapboxGeocoder.prototype = {
enableGeolocation: false,
addressAccuracy: 'street',
useBrowserFocus: false,
inputTransforms: {
trimCoordinatesPunctuation: false
},
getItemValue: function(item) {
return item.place_name
},
render: function(item) {
var placeName = escapeHtml(item.place_name).split(',');
return '<div class="mapboxgl-ctrl-geocoder--suggestion"><div class="mapboxgl-ctrl-geocoder--suggestion-title">' + placeName[0]+ '</div><div class="mapboxgl-ctrl-geocoder--suggestion-address">' + placeName.splice(1, placeName.length).join(',') + '</div></div>';
}
},

_headers: {},
};

this._eventEmitter = new EventEmitter();

this.options = extend({}, defaultOptions, options);

if (options && options.inputTransforms) {
this.options.inputTransforms = extend({}, defaultOptions.inputTransforms, options.inputTransforms);
}

this.inputString = '';
this.fresh = true;
this.lastSelected = null;
this.geolocation = new Geolocation();
this._headers = {};
}

function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

MapboxGeocoder.prototype = {
/**
* Add the geocoder to a container. The container can be either a `mapboxgl.Map`, an `HTMLElement` or a CSS selector string.
*
Expand Down Expand Up @@ -797,8 +809,34 @@ MapboxGeocoder.prototype = {
return config;
},

/**
* Recursively applies transformations to the search input until a pass leaves it unchanged.
* @param {String} searchInput
* @returns {String} the transformed search input
* @private
*/
_transformInput: function(searchInput) {
let transformed = searchInput.trim();

if (this.options.inputTransforms.trimCoordinatesPunctuation && utils.RELAXED_COORD_RGX.test(transformed)) {
if (PUNCTUATION_CHARS.has(transformed.charAt(0))) {
transformed = transformed.slice(1);
}

if (PUNCTUATION_CHARS.has(transformed.charAt(transformed.length - 1))) {
transformed = transformed.slice(0, -1);
}
}

if (transformed !== searchInput) {
return this._transformInput(transformed);
}

return transformed;
},

_geocode: function(searchInput) {
searchInput = searchInput.trim();
searchInput = this._transformInput(searchInput);
this.inputString = searchInput;
this._showLoadingIcon();
this._eventEmitter.emit('loading', { query: searchInput });
Expand Down
5 changes: 5 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ function getAddressInfo(feature) {

const REVERSE_GEOCODE_COORD_RGX = /^(-?\d{1,3}(\.\d{0,256})?)[, ]+(-?\d{1,3}(\.\d{0,256})?)$/;

// Unanchored version of REVERSE_GEOCODE_COORD_RGX: checks that the string contains
// coordinates somewhere in it, regardless of surrounding punctuation/whitespace.
const RELAXED_COORD_RGX = /(-?\d{1,3}(\.\d{0,256})?)[, ]+(-?\d{1,3}(\.\d{0,256})?)/;

module.exports = {
transformFeatureToGeolocationText: transformFeatureToGeolocationText,
getAddressInfo: getAddressInfo,
REVERSE_GEOCODE_COORD_RGX: REVERSE_GEOCODE_COORD_RGX,
RELAXED_COORD_RGX: RELAXED_COORD_RGX,
}
96 changes: 96 additions & 0 deletions test/test.geocoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,102 @@ test('geocoder', function(tt) {
);
});

tt.test('options.inputTransforms.trimCoordinatesPunctuation - false by default', function(t) {
setup();
t.equals(geocoder.options.inputTransforms.trimCoordinatesPunctuation, false, 'trimCoordinatesPunctuation defaults to false');
t.end();
});

tt.test('options.inputTransforms.trimCoordinatesPunctuation - false does not trim punctuation from coordinate-like input', function(t) {
t.plan(1);
setup({});
geocoder.query(';48.774989, 9.155557;');
geocoder.on(
'results',
once(function() {
t.equals(geocoder.inputString, ';48.774989, 9.155557;', 'inputString keeps punctuation when trimCoordinatesPunctuation is disabled');
})
);
});

tt.test('options.inputTransforms.trimCoordinatesPunctuation - true trims leading punctuation from coordinate-like input', function(t) {
t.plan(1);
setup({
inputTransforms: { trimCoordinatesPunctuation: true }
});
geocoder.query(';48.774989, 9.155557');
geocoder.on(
'results',
once(function() {
t.equals(geocoder.inputString, '48.774989, 9.155557', 'leading punctuation is trimmed from coordinate-like input');
})
);
});

tt.test('options.inputTransforms.trimCoordinatesPunctuation - true trims trailing punctuation from coordinate-like input', function(t) {
t.plan(1);
setup({
inputTransforms: { trimCoordinatesPunctuation: true }
});
geocoder.query('48.774989, 9.155557;');
geocoder.on(
'results',
once(function() {
t.equals(geocoder.inputString, '48.774989, 9.155557', 'trailing punctuation is trimmed from coordinate-like input');
})
);
});

tt.test('options.inputTransforms.trimCoordinatesPunctuation - true recursively trims repeated punctuation and whitespace', function(t) {
t.plan(1);
setup({
inputTransforms: { trimCoordinatesPunctuation: true }
});
geocoder.query(' ;;48.774989, 9.155557;; ');
geocoder.on(
'results',
once(function() {
t.equals(geocoder.inputString, '48.774989, 9.155557', 'repeated punctuation and whitespace are trimmed');
})
);
});

tt.test('options.inputTransforms.trimCoordinatesPunctuation - true does not trim punctuation from non-coordinate input', function(t) {
t.plan(1);
setup({
inputTransforms: { trimCoordinatesPunctuation: true }
});
geocoder.query('Paris;');
geocoder.on(
'results',
once(function() {
t.equals(geocoder.inputString, 'Paris;', 'punctuation is left untouched for non-coordinate input');
})
);
});

tt.test('options.inputTransforms.trimCoordinatesPunctuation - true does not trim punctuation in the middle of coordinate-like input', function(t) {
t.plan(1);
setup({
inputTransforms: { trimCoordinatesPunctuation: true }
});
geocoder.query('note;48.774989, 9.155557;end');
geocoder.on(
'results',
once(function() {
t.equals(geocoder.inputString, 'note;48.774989, 9.155557;end', 'punctuation in the middle of the string is left untouched');
})
);
});

tt.test('options.inputTransforms - partial object merges with defaults', function(t) {
setup({
inputTransforms: {}
});
t.equals(geocoder.options.inputTransforms.trimCoordinatesPunctuation, false, 'trimCoordinatesPunctuation still defaults to false when a partial inputTransforms object is provided');
t.end();
});

tt.test('custom endpoint', function(t) {
t.plan(1);
setup({ origin: 'localhost:2999' });
Expand Down
Loading