Skip to content

Commit c79a91a

Browse files
committed
Update search, suggestions map-link skill
1 parent 22c183b commit c79a91a

2 files changed

Lines changed: 206 additions & 2 deletions

File tree

.github/skills/map-link-markup/SKILL.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ defines several uses of existing and new `rel` keyword values.
3737
| `zoomout` | The link `href` is followed automatically by the polyfill when the map is zoomed out by the user to a value less than the minimum value of the zoom range of the current layer. The referenced map layer resource replaces the current map layer. The polyfill does not represent this link as a user-visible affordance, it is followed automatically. If the remote resource does not contain a reciprocal `zoomin` link, the map state change is one-way i.e. the layer is permanently replaced. |
3838
| `legend` | The `legend` link relation designates a link to metadata, typically an image, describing the symbology used by the current layer. Currently, the polyfill creates a hyperlink for the label of the layer in the layer control, which opens in a new browsing context. |
3939
| `query` | The `query` link relation is used in combination with the `tref="..."` attribute to establish a URL template that composes a map query URL based on user map gestures such as click or touch. These URLs are fetched and the response presented on top of the map as a popup. Such queries can return text/html or text/mapml responses. In the latter case, the response may contain more than one feature, in which case a 'paged' popup is generated, allowing the user to cycle through the features' individual metadata. |
40+
| `search` | The `search` link relation is used with the `tref="..."` attribute to define a URL template for a search endpoint. The template must contain the `{searchTerms}` variable reference, which is replaced with the user's URL-encoded search query. The search is triggered when the user presses Enter or clicks a suggestion. The response is expected to be a GeoJSON `FeatureCollection` (the default handler format). Only the first `<map-link rel="search">` per `<map-layer>` is honored. The link must be a direct child of `<map-layer>` (for local/inline layers) or placed inside `<map-head>` (for remote `.mapml` layers). The search control is opt-in via `controlslist="search"` on the `<mapml-viewer>` or `<map is="web-map">` element. The search button is disabled when no visible (checked) layer provides a `<map-link rel="search">`. |
41+
| `suggestions` | The `suggestions` link relation is used with the `tref="..."` attribute to define a URL template for a suggestions/autocomplete endpoint. Like `search`, the template must contain the `{searchTerms}` variable reference. Suggestions are fetched automatically as the user types (debounced, minimum 2 characters). The default handler expects a GeoJSON `FeatureCollection` response, rendering each feature as a clickable result button (using `properties.display_name` or `properties.name`). Only the first `<map-link rel="suggestions">` per `<map-layer>` is honored. Suggestions are optional — if no `rel="suggestions"` link exists, only Enter triggers a search. |
4042
| `stylesheet` | The link imports a CSS stylesheet from the `href` value. |
4143

4244

@@ -135,4 +137,206 @@ Projection values [defined by the polyfill](../mapml-viewer#projection) include:
135137
</map-extent>
136138
</layer->
137139
</mapml-viewer>
140+
```
141+
142+
### Search and Suggestions
143+
144+
The search control is opt-in: add `controlslist="search"` to your `<mapml-viewer>`
145+
or `<map is="web-map">` element. A magnifying-glass button appears in the
146+
top-left controls. The button is disabled (grayed out, `aria-disabled="true"`)
147+
when no visible (checked) `<map-layer>` has a descendant `<map-link rel="search">`.
148+
149+
The `{searchTerms}` template variable in `tref` is the only required variable.
150+
It is replaced with the user's URL-encoded query string. No sibling
151+
`<map-input>` elements are needed for search/suggestions links.
152+
153+
#### Placement rules
154+
155+
- **Inline (local) layers:** place `<map-link rel="search">` and
156+
`<map-link rel="suggestions">` as direct children of `<map-layer>`.
157+
- **Remote layers (`.mapml` files):** place them inside `<map-head>`.
158+
- Do **not** place search/suggestions links inside `<map-extent>`.
159+
- Only the **first** `<map-link rel="search">` and first
160+
`<map-link rel="suggestions">` per `<map-layer>` are honored.
161+
- Multiple layers may each contribute their own search/suggestions links;
162+
responses are merged.
163+
164+
#### Default handler format (GeoJSON)
165+
166+
The default handler expects a **GeoJSON `FeatureCollection`** response from
167+
both search and suggestions endpoints. Each `Feature` should include:
168+
169+
- `geometry` with coordinates (used for `setView` fallback)
170+
- `bbox` (4-element array `[west, south, east, north]`; used for `fitBounds`)
171+
- `properties.display_name` or `properties.name` (rendered as button text)
172+
173+
This format is compatible with Nominatim (`format=geojson`) and Photon out of
174+
the box. Example minimal response:
175+
176+
```json
177+
{
178+
"type": "FeatureCollection",
179+
"features": [
180+
{
181+
"type": "Feature",
182+
"bbox": [-75.76, 45.35, -75.62, 45.46],
183+
"geometry": { "type": "Point", "coordinates": [-75.69, 45.42] },
184+
"properties": { "display_name": "Ottawa, Ontario, Canada" }
185+
}
186+
]
187+
}
188+
```
189+
190+
#### Default handler — inline layer example
191+
192+
```html
193+
<mapml-viewer projection="OSMTILE" zoom="14" lat="45.4" lon="-75.7"
194+
controls controlslist="search">
195+
<map-layer label="OpenStreetMap" checked>
196+
<!-- suggestions fetched as the user types (debounced, min 2 chars) -->
197+
<map-link rel="suggestions"
198+
tref="https://nominatim.openstreetmap.org/search?q={searchTerms}&format=geojson&limit=5"></map-link>
199+
<!-- search fetched on Enter -->
200+
<map-link rel="search"
201+
tref="https://nominatim.openstreetmap.org/search?q={searchTerms}&format=geojson&limit=10"></map-link>
202+
<map-extent units="OSMTILE" checked>
203+
<map-input name="z" type="zoom" min="0" max="18"></map-input>
204+
<map-input name="x" type="location" units="tilematrix" axis="column"></map-input>
205+
<map-input name="y" type="location" units="tilematrix" axis="row"></map-input>
206+
<map-link rel="tile" tref="https://tile.openstreetmap.org/{z}/{x}/{y}.png"></map-link>
207+
</map-extent>
208+
</map-layer>
209+
</mapml-viewer>
210+
```
211+
212+
#### Default handler — remote layer example
213+
214+
In the HTML page:
215+
216+
```html
217+
<mapml-viewer projection="CBMTILE" zoom="5" lat="45.4" lon="-75.7"
218+
controls controlslist="search">
219+
<map-layer label="Canada Base Map" src="canada.mapml" checked></map-layer>
220+
</mapml-viewer>
221+
```
222+
223+
In `canada.mapml`:
224+
225+
```xml
226+
<mapml->
227+
<map-head>
228+
<map-meta charset="utf-8"></map-meta>
229+
<map-link rel="suggestions" tref="https://geogratis.gc.ca/services/geoname/en/geonames.json?q={searchTerms}*&num=20"></map-link>
230+
<map-link rel="search" tref="https://geogratis.gc.ca/services/geoname/en/geonames.json?q={searchTerms}&num=20"></map-link>
231+
</map-head>
232+
<map-body>
233+
<map-extent units="CBMTILE" checked>
234+
<map-input name="z" type="zoom" min="0" max="17"></map-input>
235+
<map-input name="y" type="location" units="tilematrix" axis="row"></map-input>
236+
<map-input name="x" type="location" units="tilematrix" axis="column"></map-input>
237+
<map-link rel="tile" tref="https://example.com/tiles/{z}/{y}/{x}.png"></map-link>
238+
</map-extent>
239+
</map-body>
240+
</mapml->
241+
```
242+
243+
#### Custom handler — overriding the default with `preventDefault()`
244+
245+
When the server's response format does not match GeoJSON `FeatureCollection`,
246+
use `preventDefault()` on the `mapsuggestions` and/or `mapsearch` events to
247+
suppress the default handler and render results yourself.
248+
249+
The polyfill dispatches two cancelable `CustomEvent`s on the `<mapml-viewer>`
250+
(or `<map is="web-map">`) element:
251+
252+
| Event | Fires when | `e.detail` properties |
253+
|-------------------|-------------------------------------------------|-----------------------------------|
254+
| `mapsuggestions` | Suggestion responses arrive (user is typing) | `query`, `responses` |
255+
| `mapsearch` | Search responses arrive (user pressed Enter) | `query`, `responses` |
256+
257+
`e.detail.responses` is an array of `{ data, link, layer }` objects — one per
258+
layer that contributed a link. `data` is the parsed JSON response body; `link`
259+
is the `<map-link>` element; `layer` is the `<map-layer>` element.
260+
261+
To render results, query the search panel's results container from the map and
262+
append your own HTML. Results should use the class `mapml-search-result` on
263+
`<button>` elements for consistent styling.
264+
265+
Example using the geonames.gc.ca API (non-GeoJSON response shape):
266+
267+
```html
268+
<mapml-viewer projection="CBMTILE" zoom="5" lat="45.4" lon="-75.7"
269+
controls controlslist="search">
270+
<map-layer label="Geonames Layer" src="geonames-layer.mapml" checked></map-layer>
271+
</mapml-viewer>
272+
273+
<script>
274+
const viewer = document.querySelector('mapml-viewer');
275+
276+
viewer.addEventListener('mapsuggestions', (e) => {
277+
e.preventDefault();
278+
// Access the results container via the map's internal DOM
279+
const container = viewer._map._container.querySelector('.mapml-search-results');
280+
container.innerHTML = '';
281+
for (const { data } of e.detail.responses) {
282+
if (!data || !data.items) continue;
283+
for (const item of data.items) {
284+
const btn = document.createElement('button');
285+
btn.className = 'mapml-search-result';
286+
btn.setAttribute('type', 'button');
287+
btn.textContent = item.name;
288+
btn.addEventListener('click', () => {
289+
if (item.bbox && item.bbox.length === 4) {
290+
const [west, south, east, north] = item.bbox;
291+
viewer._map.fitBounds([[south, west], [north, east]]);
292+
} else {
293+
viewer._map.setView([item.latitude, item.longitude], 10);
294+
}
295+
});
296+
container.appendChild(btn);
297+
}
298+
}
299+
});
300+
301+
viewer.addEventListener('mapsearch', (e) => {
302+
e.preventDefault();
303+
// Same pattern — parse the non-standard response and render results
304+
const container = viewer._map._container.querySelector('.mapml-search-results');
305+
container.innerHTML = '';
306+
for (const { data } of e.detail.responses) {
307+
if (!data || !data.items) continue;
308+
for (const item of data.items) {
309+
const btn = document.createElement('button');
310+
btn.className = 'mapml-search-result';
311+
btn.setAttribute('type', 'button');
312+
btn.textContent = item.name;
313+
btn.addEventListener('click', () => {
314+
if (item.bbox && item.bbox.length === 4) {
315+
const [west, south, east, north] = item.bbox;
316+
viewer._map.fitBounds([[south, west], [north, east]]);
317+
} else {
318+
viewer._map.setView([item.latitude, item.longitude], 10);
319+
}
320+
});
321+
container.appendChild(btn);
322+
}
323+
}
324+
});
325+
</script>
326+
```
327+
328+
#### Search without suggestions
329+
330+
Suggestions are optional. If only `<map-link rel="search">` is provided
331+
(no `rel="suggestions"`), the control will not fetch anything as the user
332+
types — only pressing Enter will trigger a search request.
333+
334+
```html
335+
<map-layer label="Search Only" checked>
336+
<map-link rel="search"
337+
tref="https://nominatim.openstreetmap.org/search?q={searchTerms}&format=geojson&limit=10"></map-link>
338+
<map-extent units="OSMTILE" checked>
339+
<!-- ... map-inputs and tile link ... -->
340+
</map-extent>
341+
</map-layer>
138342
```

search-implementation-plan.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ Reference design: search.md. Example service response: geonames.json.
9393
## Implementation Notes
9494

9595
- `{searchTerms}` substitution: simple string replace on `tref`.
96-
- Search icon: first control in top-left (created before zoom).
97-
- Panel inside shadow DOM → inherits mapml.css.
96+
- Search icon: second control in top-left (created after zoom).
97+
- Panel inside shadow DOM → inherits mapml.css, grabs focus from map for scroll behaviour
9898

9999
## Out of Scope
100100

0 commit comments

Comments
 (0)