diff --git a/src/common/SearchQueryParser.ts b/src/common/SearchQueryParser.ts index 97efd52a..34df8f33 100644 --- a/src/common/SearchQueryParser.ts +++ b/src/common/SearchQueryParser.ts @@ -340,13 +340,38 @@ export class SearchQueryParser { ) { from = from.slice(1, from.length - 1); } + + // Check if the from part matches coordinate pattern (number, number) + const coordMatch = from.match(/^\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*$/); + if (coordMatch) { + // It's a coordinate pair + const latitude = parseFloat(coordMatch[1]); + const longitude = parseFloat(coordMatch[2]); + return { + type: SearchQueryTypes.distance, + distance: intFromRegexp(str), + from: { + GPSData: { + latitude, + longitude + } + }, + // only add negate if the value is true + ...(new RegExp('^\\d*-' + this.keywords.kmFrom + '!:').test(str) && { + negate: true, + }), + } as DistanceSearch; + } + + // If not coordinates, treat as location text return { type: SearchQueryTypes.distance, distance: intFromRegexp(str), from: {text: from}, + // only add negate if the value is true ...(new RegExp('^\\d*-' + this.keywords.kmFrom + '!:').test(str) && { negate: true, - }), // only add if the value is true + }), } as DistanceSearch; } @@ -557,25 +582,28 @@ export class SearchQueryParser { ? '' : (query as RangeSearch).value) ); - case SearchQueryTypes.distance: - if ((query as DistanceSearch).from.text.indexOf(' ') !== -1) { - return ( - (query as DistanceSearch).distance + - '-' + - this.keywords.kmFrom + - colon + - '(' + - (query as DistanceSearch).from.text + - ')' - ); + case SearchQueryTypes.distance: { + const distanceQuery = query as DistanceSearch; + const text = distanceQuery.from.text; + const coords = distanceQuery.from.GPSData; + + let locationStr = ''; + if (text) { + // If we have location text, use that + locationStr = text; + } else if (coords && coords.latitude != null && coords.longitude != null) { + // If we only have coordinates, use them + locationStr = `${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)}`; } - return ( - (query as DistanceSearch).distance + - '-' + - this.keywords.kmFrom + - colon + - (query as DistanceSearch).from.text - ); + + // Add brackets if the location string contains spaces + if (locationStr.indexOf(' ') !== -1) { + locationStr = `(${locationStr})`; + } + + return `${distanceQuery.distance}-${this.keywords.kmFrom}${colon}${locationStr}`; + } + case SearchQueryTypes.orientation: return ( this.keywords.orientation + diff --git a/test/common/unit/SearchQueryParser.ts b/test/common/unit/SearchQueryParser.ts index 02f551b2..41036251 100644 --- a/test/common/unit/SearchQueryParser.ts +++ b/test/common/unit/SearchQueryParser.ts @@ -112,8 +112,106 @@ describe('SearchQueryParser', () => { check({type: SearchQueryTypes.max_resolution, value: 5, negate: true} as MaxResolutionSearch); }); it('Distance search', () => { + // Test location-based distance search check({type: SearchQueryTypes.distance, distance: 10, from: {text: 'New York'}} as DistanceSearch); check({type: SearchQueryTypes.distance, distance: 10, from: {text: 'New York'}, negate: true} as DistanceSearch); + + // Test coordinate-based distance search + check({ + type: SearchQueryTypes.distance, + distance: 5, + from: { + GPSData: { + latitude: 40.712776, + longitude: -74.005974 + } + } + } as DistanceSearch); + + // Test coordinate-based distance search with negation + check({ + type: SearchQueryTypes.distance, + distance: 5, + from: { + GPSData: { + latitude: 40.712776, + longitude: -74.005974 + } + }, + negate: true + } as DistanceSearch); + + // Test parsing specific coordinate formats + const parser = new SearchQueryParser(defaultQueryKeywords); + + // Test basic coordinate format + expect(parser.parse('5-km-from:(40.712776, -74.005974)')).to.deep.equals({ + type: SearchQueryTypes.distance, + distance: 5, + from: { + GPSData: { + latitude: 40.712776, + longitude: -74.005974 + } + } + } as DistanceSearch); + + // Test coordinate format with extra spaces + expect(parser.parse('5-km-from:( 40.712776 , -74.005974 )')).to.deep.equals({ + type: SearchQueryTypes.distance, + distance: 5, + from: { + GPSData: { + latitude: 40.712776, + longitude: -74.005974 + } + } + } as DistanceSearch); + + // Test coordinates with different decimal places + expect(parser.parse('5-km-from:(40.7, -74.1)')).to.deep.equals({ + type: SearchQueryTypes.distance, + distance: 5, + from: { + GPSData: { + latitude: 40.7, + longitude: -74.1 + } + } + } as DistanceSearch); + + // Test negated coordinate search + expect(parser.parse('5-km-from!:(40.712776, -74.005974)')).to.deep.equals({ + type: SearchQueryTypes.distance, + distance: 5, + from: { + GPSData: { + latitude: 40.712776, + longitude: -74.005974 + } + }, + negate: true + } as DistanceSearch); + + // Test stringification of coordinates + const query: DistanceSearch = { + type: SearchQueryTypes.distance, + distance: 5, + from: { + GPSData: { + latitude: 40.712776, + longitude: -74.005974 + } + } + }; + expect(parser.stringify(query)).to.equals('5-km-from:(40.712776, -74.005974)'); + + // Test stringification of negated coordinates + const negatedQuery: DistanceSearch = { + ...query, + negate: true + }; + expect(parser.stringify(negatedQuery)).to.equals('5-km-from!:(40.712776, -74.005974)'); }); it('OrientationSearch search', () => { check({type: SearchQueryTypes.orientation, landscape: true} as OrientationSearch);