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
133 changes: 44 additions & 89 deletions src/geosearch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { request, App } from 'obsidian';
import { App } from 'obsidian';
import * as geosearch from 'leaflet-geosearch';
import * as leaflet from 'leaflet';
import * as querystring from 'query-string';

import { PluginSettings } from 'src/settings';
import { UrlConvertor } from 'src/urlConvertor';
import { FileMarker } from 'src/markers';
import * as consts from 'src/consts';
import { GooglePlacesAPI } from './geosearchGoogleApi';

/**
* A generic result of a geosearch
Expand All @@ -19,10 +19,18 @@ export class GeoSearchResult {
existingMarker?: FileMarker;
}

type searchProviderParms = {
query: string;
location?: string;
};

export type GeoSearcherProvider =
| geosearch.OpenStreetMapProvider
| geosearch.GoogleProvider
| GooglePlacesAPI;

export class GeoSearcher {
private searchProvider:
| geosearch.OpenStreetMapProvider
| geosearch.GoogleProvider = null;
public searchProvider: GeoSearcherProvider;
private settings: PluginSettings;
private urlConvertor: UrlConvertor;

Expand All @@ -31,7 +39,9 @@ export class GeoSearcher {
this.urlConvertor = new UrlConvertor(app, settings);
if (settings.searchProvider == 'osm')
this.searchProvider = new geosearch.OpenStreetMapProvider();
else if (settings.searchProvider == 'google') {
else if (this.usingGooglePlacesSearch) {
this.searchProvider = new GooglePlacesAPI(settings);
} else if (settings.searchProvider == 'google') {
this.searchProvider = new geosearch.GoogleProvider({
params: { key: settings.geocodingApiKey },
});
Expand Down Expand Up @@ -59,97 +69,42 @@ export class GeoSearcher {
});
}

// Google Place results
if (
this.settings.searchProvider == 'google' &&
this.settings.useGooglePlaces &&
this.settings.geocodingApiKey
) {
try {
const placesResults = await googlePlacesSearch(
query,
this.settings,
searchArea?.getCenter()
);
for (const result of placesResults)
results.push({
name: result.name,
location: result.location,
resultType: 'searchResult',
});
} catch (e) {
console.log(
'Map View: Google Places search failed: ',
e.message
);
}
} else {
const areaSW = searchArea?.getSouthWest() || null;
const areaNE = searchArea?.getNorthEast() || null;
let searchResults = await this.searchProvider.search({
query: query,
});
let params: searchProviderParms = {
query: query,
};

if (this.usingGooglePlacesSearch() && searchArea) {
const centerOfSearch = searchArea?.getCenter();
params.location = `${centerOfSearch.lat},${centerOfSearch.lng}`;
}
let searchResults = await this.searchProvider.search(params);

if (!this.usingGooglePlacesSearch()) {
searchResults = searchResults.slice(
0,
consts.MAX_EXTERNAL_SEARCH_SUGGESTIONS
);
results = results.concat(
searchResults.map(
(result) =>
({
name: result.label,
location: new leaflet.LatLng(result.y, result.x),
resultType: 'searchResult',
} as GeoSearchResult)
)
);
}

results = results.concat(
searchResults.map(
(result) =>
({
name: result.label,
location: new leaflet.LatLng(result.y, result.x),
resultType: 'searchResult',
} as GeoSearchResult)
)
);

return results;
}
}

export async function googlePlacesSearch(
query: string,
settings: PluginSettings,
centerOfSearch: leaflet.LatLng | null
): Promise<GeoSearchResult[]> {
if (settings.searchProvider != 'google' || !settings.useGooglePlaces)
return [];
const googleApiKey = settings.geocodingApiKey;
const params = {
query: query,
key: googleApiKey,
};
if (centerOfSearch)
(params as any)[
'location'
] = `${centerOfSearch.lat},${centerOfSearch.lng}`;
const googleUrl =
'https://maps.googleapis.com/maps/api/place/textsearch/json?' +
querystring.stringify(params);
const googleContent = await request({ url: googleUrl });
const jsonContent = JSON.parse(googleContent) as any;
let results: GeoSearchResult[] = [];
if (
jsonContent &&
'results' in jsonContent &&
jsonContent?.results.length > 0
) {
for (const result of jsonContent.results) {
const location = result.geometry?.location;
if (location && location.lat && location.lng) {
const geolocation = new leaflet.LatLng(
location.lat,
location.lng
);
results.push({
name: `${result?.name} (${result?.formatted_address})`,
location: geolocation,
resultType: 'searchResult',
} as GeoSearchResult);
}
}
usingGooglePlacesSearch(): boolean {
return (
this.settings.searchProvider == 'google' &&
this.settings.useGooglePlaces &&
this.settings.geocodingApiKey !== null
);
}
return results;
}
62 changes: 62 additions & 0 deletions src/geosearchGoogleApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { PluginSettings } from './settings';
import * as leaflet from 'leaflet';
import { request } from 'obsidian';
import * as querystring from 'query-string';
import { GeoSearchResult } from './geosearch';
import { SearchResult } from 'leaflet-geosearch/dist/providers/provider';

export type GooglePlacesAPIQuery = { [key: string]: string };

export class GooglePlacesAPI {
private googleApiKey: string;

constructor(settings: PluginSettings) {
this.googleApiKey = settings.geocodingApiKey;
}

async googlePlacesRequest(
query: GooglePlacesAPIQuery,
scope: string
): Promise<{}> {
query.key = this.googleApiKey;
const googleUrl = `https://maps.googleapis.com/maps/api/place/${scope}/json?${querystring.stringify(
query
)}`;
const googleContent = await request({ url: googleUrl });
return JSON.parse(googleContent) as any;
}

async search(query: GooglePlacesAPIQuery): Promise<SearchResult[]> {
let jsonContent: any = await this.googlePlacesSearch(query);

let results: SearchResult[] = [];
if (
jsonContent &&
'results' in jsonContent &&
jsonContent?.results.length > 0
) {
for (const result of jsonContent.results) {
const location = result.geometry?.location;
if (location && location.lat && location.lng) {
results.push({
label: `${result?.name} (${result?.formatted_address})`,
y: location.lat,
x: location.lng,
} as any);
}
}
}
return results;
}

async googlePlacesSearch(query: GooglePlacesAPIQuery): Promise<{}> {
return await this.googlePlacesRequest(query, 'textsearch');
}

async googlePlacesDetailsSearch(placeId: string): Promise<{}> {
const params = {
place_id: placeId,
};
return this.googlePlacesRequest(params, 'details');
}
}
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ import { SettingsTab } from 'src/settingsTab';
import { LocationSearchDialog } from 'src/locationSearchDialog';
import { TagSuggest } from 'src/tagSuggest';
import * as utils from 'src/utils';
import { MapViewAPI } from './mapViewAPI';

export default class MapViewPlugin extends Plugin {
settings: PluginSettings;
public highestVersionSeen: number = 0;
public iconCache: IconCache;
public mapViewAPI: MapViewAPI;
private suggestor: LocationSuggest;
private tagSuggestor: TagSuggest;
private urlConvertor: UrlConvertor;
Expand Down Expand Up @@ -138,6 +140,7 @@ export default class MapViewPlugin extends Plugin {
);

this.suggestor = new LocationSuggest(this.app, this.settings);
this.mapViewAPI = new MapViewAPI(this.app, this.settings);
this.tagSuggestor = new TagSuggest(this.app, this.settings);
this.urlConvertor = new UrlConvertor(this.app, this.settings);

Expand Down
13 changes: 13 additions & 0 deletions src/mapviewAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { App } from 'obsidian';
import { GeoSearcher, GeoSearcherProvider } from './geosearch';
import { PluginSettings } from 'src/settings';

export class MapViewAPI {
public searchProvider: GeoSearcherProvider;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about the misunderstanding; that's not what I meant in our discussion.
If you expose the searchProvider and searcher fields as an API this way, then you actually expose geosearch.OpenStreetMapProvider, geosearch.GoogleProvider, GooglePlacesAPI and GeoSearcher, and all of then now become an API that must be maintained. Every single thing we ever change in GeoSearcher is a potential API breakage and that's a lot of maintenance burden to consider.
What I think we should do instead is that this MapViewAPI class exposes a single method geosearch with the least dependencies possible, or maybe 2-3 methods if you want to provide some more functionality, and mask away all of the implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason I want to expose those API calls directly is that abstracting them away again makes it hard to manipulate the raw data (which in my case I want to do, understand if this isn't the direction you want to take it).

How would you suggest exposing the raw response from the search API and things like the Google Places API details endpoints?

Other then exposing the searchProvider directly I don't see a nice way.

If those APIs change would the release not just be a breaking change.

public searcher: GeoSearcher;

constructor(app: App, settings: PluginSettings) {
this.searcher = new GeoSearcher(app, settings);
this.searchProvider = this.searcher.searchProvider;
}
}