Add path/route layer type + online geocoding + Rogers County example data

- custom-layers.mjs: new 'path' layerType (LineStrings like GPX tracks/trails)
  with a 'line' feature format, an example trails layer, and a seeded
  Rogers County POI layer with real geocoded coordinates (museums, courthouse,
  university, towns)
- osm-data.mjs: converts path features to GeoJSON LineStrings and tags
  layerType='path' in the manifest; fixes kind detection for all three types
- MapView.svelte: styles path layers as dashed polylines, computes bounds for
  LineStrings, supports 'path' in search results, and adds a Nominatim
  'Places (online)' geocoding fallback section to the search box (debounced,
  fit-to-bounds on select)
- geocode.ts: dependency-free Nominatim client
- README: documents path layers and online location search
This commit is contained in:
hermes-explorigin 2026-08-08 18:26:48 +00:00
parent 63f1b75456
commit 4e3fef6559
10 changed files with 312 additions and 32 deletions

View File

@ -89,18 +89,19 @@ parks: `
List configured queries with `npm run data -- --queries`, or build a single area List configured queries with `npm run data -- --queries`, or build a single area
with `npm run data -- --area=tulsa`. with `npm run data -- --area=tulsa`.
## Custom Layers (Zones & Points of Interest) ## Custom Layers (Zones, Points of Interest & Paths)
Besides Overpass-fetched data, you can define your own layers by hand in Besides Overpass-fetched data, you can define your own layers by hand in
**`scripts/custom-layers.mjs`**. This file ships with a couple of example layers **`scripts/custom-layers.mjs`**. It ships with several example layers
(`my-places` POIs and `districts` zones) that you can extend or replace. (`my-places` POIs, `districts` zones, `trails` path, and `rogers-county` local
POIs) that you can extend or replace.
Each layer has: Each layer has:
- `id` — unique slug (becomes the output filename) - `id` — unique slug (becomes the output filename)
- `name` — label shown in the UI legend - `name` — label shown in the UI legend
- `layerType``'zone'` (polygon) or `'poi'` (point) - `layerType``'poi'` (point), `'zone'` (polygon), or `'path'` (LineString)
- `color` — hex color used for markers/fill - `color` — hex color used for markers/fill/lines
- `features` — array of feature objects - `features` — array of feature objects
POI (point) — optional `address`, `bearing`, and `fov` fields: POI (point) — optional `address`, `bearing`, and `fov` fields:
@ -139,15 +140,35 @@ Zone (polygon — the ring is closed for you):
} }
``` ```
Path / route (LineString — an ordered list of points like a GPX track):
```js
{
name: 'Claremore Lake Loop',
desc: 'Example walking route around the lake',
line: [
[-95.5649, 36.3415],
[-95.5700, 36.3430],
[-95.5750, 36.3420],
[-95.5720, 36.3360]
]
}
```
Paths render as dashed polylines and are searchable by name.
Coordinates are `[longitude, latitude]` (lon first, like GeoJSON). The file also Coordinates are `[longitude, latitude]` (lon first, like GeoJSON). The file also
includes a copy-paste TEMPLATE block for adding new layers. After editing, run includes a copy-paste TEMPLATE block for adding new layers. After editing, run
`npm run build:data` (or `npm run build`) to regenerate the static data. `npm run build:data` (or `npm run build`) to regenerate the static data.
## Search ## Search
The webapp includes a **fuzzy search** in the top bar that matches POIs and The webapp includes a **fuzzy search** in the top bar. It matches your POIs,
zones by **name or address**. Typing at least two characters shows ranked zones, and paths by **name or address** — and, as a fallback, queries the
results; selecting one pans/zooms the map to it and opens its popup. [Nominatim](https://nominatim.openstreetmap.org/) OSM geocoder so you can also
search for **any location by name** (cities, landmarks, addresses anywhere). A
"Places (online)" section appears beneath the local matches; selecting a result
pans/zooms the map to it (fit-to-bounds for places with a bounding box).
The matcher: The matcher:
- Normalizes text (case, punctuation, and common address words — `st``street`, - Normalizes text (case, punctuation, and common address words — `st``street`,

View File

@ -1,9 +1,10 @@
/** /**
* Custom Layers Configuration * Custom Layers Configuration
* =========================== * ===========================
* Define your own map layers here **zones** (polygons/areas) and * Define your own map layers here **zones** (polygons/areas),
* **points of interest** (POIs). These are bundled into the static build * **points of interest** (POIs), and **paths/routes** (LineStrings like GPX
* alongside the Overpass OSM data. * tracks & trails). These are bundled into the static build alongside the
* Overpass OSM data.
* *
* This file is meant to be edited by hand. To add features, just append * This file is meant to be edited by hand. To add features, just append
* to the `features` array of an existing layer, or add a whole new layer. * to the `features` array of an existing layer, or add a whole new layer.
@ -11,8 +12,8 @@
* Each layer needs: * Each layer needs:
* - id unique slug (used for the output filename) * - id unique slug (used for the output filename)
* - name human-readable name shown in the UI * - name human-readable name shown in the UI
* - layerType 'zone' or 'poi' * - layerType 'poi' | 'zone' | 'path'
* - color hex color used for the marker/fill * - color hex color used for the marker/fill/line
* - features array of feature objects (see below) * - features array of feature objects (see below)
* *
* Feature format (by layerType): * Feature format (by layerType):
@ -22,8 +23,12 @@
* Zone (polygon list the corner points; it is closed automatically): * Zone (polygon list the corner points; it is closed automatically):
* { name, desc?, polygon: [[lon,lat], [lon,lat], ...] } * { name, desc?, polygon: [[lon,lat], [lon,lat], ...] }
* *
* Path / route (LineString an ordered list of points, like a track):
* { name, desc?, line: [[lon,lat], [lon,lat], ...] }
*
* Coordinates are [longitude, latitude] (lon first, like GeoJSON). * Coordinates are [longitude, latitude] (lon first, like GeoJSON).
* All fields except POI `lon`/`lat` (or zone `polygon`) and `name` are optional. * All fields except the geometry (`lon`/`lat`, `polygon`, or `line`) and
* `name` are optional.
* *
* Optional POI fields: * Optional POI fields:
* - address street address / place description * - address street address / place description
@ -111,18 +116,100 @@ export const CUSTOM_LAYERS = [
] ]
}, },
// -------------------------------------------------------------------------
// EXAMPLE — Path / route
// A LineString track, like a GPX route or trail. Each feature uses `line`.
// -------------------------------------------------------------------------
{
id: 'trails',
name: 'Trails & Routes',
layerType: 'path',
color: '#22c55e',
features: [
{
name: 'Claremore Lake Loop',
desc: 'Example walking route around the lake',
line: [
[-95.5649, 36.3415],
[-95.5700, 36.3430],
[-95.5750, 36.3420],
[-95.5720, 36.3360],
[-95.5660, 36.3365]
]
}
]
},
// -------------------------------------------------------------------------
// EXAMPLE — Rogers County local layer
// Real places around your area (coordinates via Nominatim). Add your own
// home / property / haunts here.
// -------------------------------------------------------------------------
{
id: 'rogers-county',
name: 'Rogers County',
layerType: 'poi',
color: '#f59e0b',
features: [
{
name: 'Claremore Museum of History',
desc: 'Local museum',
address: '121 N Weenonah Ave, Claremore, OK 74017',
lon: -95.6112936,
lat: 36.3114856
},
{
name: 'Rogers County Courthouse',
desc: 'County seat',
lon: -95.6164140,
lat: 36.3110143
},
{
name: 'Claremore Indian Hospital',
desc: 'Health center',
lon: -95.6292597,
lat: 36.3162771
},
{
name: 'Rogers State University',
desc: 'University',
address: '1701 W Will Rogers Blvd, Claremore, OK 74017',
lon: -95.6361137,
lat: 36.3186099
},
{
name: 'Lake Claremore',
lon: -95.5649169,
lat: 36.3414586
},
{
name: 'Oologah',
desc: 'Town',
lon: -95.7083151,
lat: 36.4470387
},
{
name: 'Verdigris',
desc: 'Town',
lon: -95.6910927,
lat: 36.2348197
}
]
},
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// TEMPLATE — copy this block to add a new layer // TEMPLATE — copy this block to add a new layer
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// { // {
// id: 'your-layer', // id: 'your-layer',
// name: 'Your Layer', // name: 'Your Layer',
// layerType: 'poi', // or 'zone' // layerType: 'poi', // or 'zone' | 'path'
// color: '#22c55e', // color: '#22c55e',
// features: [ // features: [
// { name: 'Example point', desc: '...', address: '1 Main St', // { name: 'Example point', desc: '...', address: '1 Main St',
// bearing: 'N', fov: 90, lon: -95.7, lat: 36.1 }, // bearing: 'N', fov: 90, lon: -95.7, lat: 36.1 },
// { name: 'Example zone', desc: '...', polygon: [[-95.7, 36.1], [-95.6, 36.1], [-95.6, 36.2], [-95.7, 36.2]] } // { name: 'Example zone', desc: '...', polygon: [[-95.7, 36.1], [-95.6, 36.1], [-95.6, 36.2], [-95.7, 36.2]] },
// { name: 'Example path', desc: '...', line: [[-95.7, 36.1], [-95.6, 36.1], [-95.6, 36.2]] }
// ] // ]
// } // }
]; ];

View File

@ -170,7 +170,7 @@ async function buildArea(name, cfg, signal) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Custom layers: convert hand-authored zones / POIs to static GeoJSON. // Custom layers: convert hand-authored POIs / zones / paths to GeoJSON.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function customFeatureToGeoJSON(f, layerId) { function customFeatureToGeoJSON(f, layerId) {
if (f.lon !== undefined && f.lat !== undefined) { if (f.lon !== undefined && f.lat !== undefined) {
@ -187,6 +187,19 @@ function customFeatureToGeoJSON(f, layerId) {
geometry: { type: 'Point', coordinates: [f.lon, f.lat] } geometry: { type: 'Point', coordinates: [f.lon, f.lat] }
}; };
} }
if (Array.isArray(f.line) && f.line.length >= 2) {
// Path / route — LineString.
const coords = f.line.map(([lon, lat]) => [lon, lat]);
const props = { name: f.name };
if (f.desc) props.description = f.desc;
if (f.address) props.address = f.address;
return {
type: 'Feature',
id: `custom/${layerId}/${f.name}`,
properties: props,
geometry: { type: 'LineString', coordinates: coords }
};
}
if (Array.isArray(f.polygon) && f.polygon.length >= 3) { if (Array.isArray(f.polygon) && f.polygon.length >= 3) {
// Polygon / zone: close the ring automatically if not closed. // Polygon / zone: close the ring automatically if not closed.
const ring = f.polygon.map(([lon, lat]) => [lon, lat]); const ring = f.polygon.map(([lon, lat]) => [lon, lat]);
@ -203,7 +216,7 @@ function customFeatureToGeoJSON(f, layerId) {
geometry: { type: 'Polygon', coordinates: [ring] } geometry: { type: 'Polygon', coordinates: [ring] }
}; };
} }
throw new Error(`Feature "${f.name}" in layer "${layerId}" is missing lon/lat (POI) or polygon[>=3] (zone)`); throw new Error(`Feature "${f.name}" in layer "${layerId}" is missing lon/lat (POI), polygon[>=3] (zone), or line[>=2] (path)`);
} }
// Normalize a compass/bearing value to a canonical numeric bearing in degrees // Normalize a compass/bearing value to a canonical numeric bearing in degrees
@ -247,7 +260,7 @@ async function buildCustomLayers() {
name: layer.id, name: layer.id,
label: layer.name, label: layer.name,
category: 'custom', category: 'custom',
layerType: layer.layerType === 'zone' ? 'zone' : 'poi', layerType: layer.layerType === 'path' ? 'path' : (layer.layerType === 'zone' ? 'zone' : 'poi'),
color: layer.color || '#e11d48', color: layer.color || '#e11d48',
featureCount: features.length, featureCount: features.length,
path: `/data/custom/${layer.id}.geojson` path: `/data/custom/${layer.id}.geojson`

View File

@ -3,6 +3,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { Map as LeafletMap } from 'leaflet'; import type { Map as LeafletMap } from 'leaflet';
import { search, type Searchable, type SearchResult } from '$lib/search'; import { search, type Searchable, type SearchResult } from '$lib/search';
import { geocode, type GeocodedPlace } from '$lib/geocode';
type FeatureCollection = { type FeatureCollection = {
type: string; type: string;
@ -13,7 +14,7 @@
name: string; // id name: string; // id
label: string; // display name label: string; // display name
category: 'osm' | 'custom'; category: 'osm' | 'custom';
layerType: 'poi' | 'zone' | 'mixed'; layerType: 'poi' | 'zone' | 'path' | 'mixed';
color: string; color: string;
featureCount: number; featureCount: number;
path: string; path: string;
@ -75,6 +76,47 @@
focusOn(r.item); focusOn(r.item);
} }
// --- online geocoding (Nominatim) as a fallback section ---
let geoResults = $state<GeocodedPlace[]>([]);
let geoLoading = $state(false);
let geoDone = $state(false);
let timer: ReturnType<typeof setTimeout>;
$effect(() => {
const q = query.trim();
if (timer) clearTimeout(timer);
if (q.length < 3) {
geoResults = [];
geoLoading = false;
geoDone = false;
return;
}
geoLoading = true;
geoDone = false;
timer = setTimeout(async () => {
try {
geoResults = await geocode(q, { countrycodes: 'us' });
} catch {
geoResults = [];
}
geoLoading = false;
geoDone = true;
}, 400);
});
function onGeoSelect(g: GeocodedPlace) {
query = g.displayName;
showResults = false;
if (g.boundingbox) {
map.fitBounds([
[g.boundingbox[0], g.boundingbox[2]],
[g.boundingbox[1], g.boundingbox[3]]
] as L.LatLngBoundsExpression, { padding: [40, 40] });
} else {
map.setView([g.lat, g.lon], 14);
}
}
function toggle(name: string) { function toggle(name: string) {
const g = layerGroups[name]; const g = layerGroups[name];
if (!g) return; if (!g) return;
@ -84,6 +126,9 @@
} }
function styleFor(type: MapLayer['layerType'], color: string): L.PathOptions { function styleFor(type: MapLayer['layerType'], color: string): L.PathOptions {
if (type === 'path') {
return { color, weight: 3, dashArray: '6 4', opacity: 0.9, fill: false };
}
if (type === 'zone') { if (type === 'zone') {
return { color, weight: 2, fillColor: color, fillOpacity: 0.2 }; return { color, weight: 2, fillColor: color, fillOpacity: 0.2 };
} }
@ -118,7 +163,7 @@
let desc = ''; let desc = '';
if (props.description) desc = `<div class="popup-desc">${String(props.description)}</div>`; if (props.description) desc = `<div class="popup-desc">${String(props.description)}</div>`;
return `<strong>${name}</strong>${desc}<table class="popup">${extra}${rows}</table><div class="popup-layer">${' '}</div>`; return `<strong>${name}</strong>${desc}<table class="popup">${extra}${rows}</table>`;
} }
onMount(async () => { onMount(async () => {
@ -142,7 +187,7 @@
if (!res.ok) throw new Error(`fetch ${layer.path}: HTTP ${res.status}`); if (!res.ok) throw new Error(`fetch ${layer.path}: HTTP ${res.status}`);
const fc: FeatureCollection = await res.json(); const fc: FeatureCollection = await res.json();
const isPoi = layer.layerType === 'poi' || (layer.category === 'custom' && layer.layerType !== 'zone'); const isPoi = layer.layerType === 'poi' || (layer.category === 'custom' && layer.layerType !== 'zone' && layer.layerType !== 'path');
const color = layer.color || '#e11d48'; const color = layer.color || '#e11d48';
const geo = L.geoJSON(fc as unknown as GeoJSON.GeoJsonObject, { const geo = L.geoJSON(fc as unknown as GeoJSON.GeoJsonObject, {
@ -176,15 +221,17 @@
let lat, lon, bounds; let lat, lon, bounds;
if (f.geometry.type === 'Point') { if (f.geometry.type === 'Point') {
[lon, lat] = f.geometry.coordinates as [number, number]; [lon, lat] = f.geometry.coordinates as [number, number];
} else if (f.geometry.type === 'Polygon') { } else if (f.geometry.type === 'Polygon' || f.geometry.type === 'LineString') {
const ring = (f.geometry.coordinates as number[][][])[0]; const coords = (f.geometry.type === 'Polygon'
const lats = ring.map((c) => c[1]); ? (f.geometry.coordinates as number[][][])[0]
const lons = ring.map((c) => c[0]); : f.geometry.coordinates as number[][]);
const lats = coords.map((c) => c[1]);
const lons = coords.map((c) => c[0]);
lat = (Math.min(...lats) + Math.max(...lats)) / 2; lat = (Math.min(...lats) + Math.max(...lats)) / 2;
lon = (Math.min(...lons) + Math.max(...lons)) / 2; lon = (Math.min(...lons) + Math.max(...lons)) / 2;
bounds = [[Math.min(...lats), Math.min(...lons)], [Math.max(...lats), Math.max(...lons)]]; bounds = [[Math.min(...lats), Math.min(...lons)], [Math.max(...lats), Math.max(...lons)]];
} }
const kind = layer.layerType === 'zone' ? 'zone' : 'poi'; const kind = layer.layerType === 'zone' ? 'zone' : (layer.layerType === 'path' ? 'path' : 'poi');
searchables.push({ searchables.push({
id: f.id as string, id: f.id as string,
name: (props.name as string) ?? String(f.id ?? ''), name: (props.name as string) ?? String(f.id ?? ''),
@ -231,12 +278,12 @@
<div class="search" role="search"> <div class="search" role="search">
<input <input
type="search" type="search"
placeholder="Search POIs &amp; zones by name or address…" placeholder="Search data or any location…"
bind:value={query} bind:value={query}
onfocus={() => (showResults = true)} onfocus={() => (showResults = true)}
oninput={() => (showResults = true)} oninput={() => (showResults = true)}
/> />
{#if results.length > 0 && showResults} {#if (results.length > 0 || geoLoading || geoResults.length > 0) && showResults}
<ul class="results"> <ul class="results">
{#each results as r (r.item.id)} {#each results as r (r.item.id)}
<li> <li>
@ -248,6 +295,19 @@
</button> </button>
</li> </li>
{/each} {/each}
{#if geoLoading}
<li class="geoload">Searching the map for locations…</li>
{:else if geoResults.length > 0}
<li class="geosep">Places (online)</li>
{#each geoResults as g, i (g.displayName + i)}
<li>
<button type="button" onmouseenter={() => onGeoSelect(g)} onclick={() => onGeoSelect(g)}>
<span class="dot" style="background:#6366f1"></span>
<span class="rname gname">{g.displayName}</span>
</button>
</li>
{/each}
{/if}
</ul> </ul>
{/if} {/if}
</div> </div>
@ -304,6 +364,9 @@
.rname { font-weight: 600; } .rname { font-weight: 600; }
.rtype { text-transform: uppercase; font-size: 0.62rem; padding: 1px 5px; border-radius: 4px; background: #e2e8f0; color: #475569; } .rtype { text-transform: uppercase; font-size: 0.62rem; padding: 1px 5px; border-radius: 4px; background: #e2e8f0; color: #475569; }
.raddr { color: #64748b; font-size: 0.7rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .raddr { color: #64748b; font-size: 0.7rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.geosep { padding: 6px 8px 2px; font-size: 0.62rem; text-transform: uppercase; letter-spacing: .05em; color: #94a3b8; }
.geoload { padding: 8px; font-size: 0.75rem; color: #64748b; font-style: italic; }
.gname { font-weight: 500; font-size: 0.78rem; line-height: 1.3; }
.map { width: 100%; height: 100%; z-index: 0; } .map { width: 100%; height: 100%; z-index: 0; }
.notice { .notice {

76
src/lib/geocode.ts Normal file
View File

@ -0,0 +1,76 @@
/**
* geocode.ts search for arbitrary places via the Nominatim OSM geocoder.
* Union with search.ts so the user can look up places that aren't in the
* static data (cities, addresses, POIs anywhere in the world).
*
* Nominatim usage policy: https://operations.osmfoundation.org/policies/nominatim/
* A descriptive User-Agent / Referer must be set. This is a client-side call,
* so we set the EA (email) and accept it may be rate-limited. For production
* you can point this at a hosted fallback or add a small server-side proxy.
*/
export interface GeocodedPlace {
displayName: string;
lat: number;
lon: number;
boundingbox?: [number, number, number, number]; // south, north, west, east
osmType: string;
osmId: number;
category: string;
kind: string;
}
const NOMINATIM_ENDPOINTS = [
'https://nominatim.openstreetmap.org/search',
'https://nominatim.openstreetmap.org/search'
];
export async function geocode(
query: string,
opts: { limit?: number; countrycodes?: string } = {}
): Promise<GeocodedPlace[]> {
const limit = opts.limit ?? 5;
const url = new URL(NOMINATIM_ENDPOINTS[0]);
url.searchParams.set('q', query);
url.searchParams.set('format', 'jsonv2');
url.searchParams.set('limit', String(limit));
if (opts.countrycodes) url.searchParams.set('countrycodes', opts.countrycodes);
let lastErr: unknown;
for (const endpoint of NOMINATIM_ENDPOINTS) {
try {
const u = new URL(endpoint);
u.search = url.search;
const res = await fetch(u.toString(), {
headers: {
Accept: 'application/json',
// Identify the client; replace with your own contact if you use this at scale.
'User-Agent': 'NavigatorOSM/0.1 (OSS map webapp; contact: local dev)'
}
});
if (!res.ok) throw new Error(`Nominatim HTTP ${res.status}`);
const data: unknown[] = await res.json();
return data
.filter((r) => r && typeof r === 'object' && 'lat' in r && 'lon' in r)
.map((r) => {
const rec = r as Record<string, unknown>;
const bb = rec.boundingbox as string[] | undefined;
return {
displayName: (rec.display_name as string) ?? '',
lat: parseFloat(rec.lat as string),
lon: parseFloat(rec.lon as string),
boundingbox: bb
? [parseFloat(bb[0]), parseFloat(bb[1]), parseFloat(bb[2]), parseFloat(bb[3])]
: undefined,
osmType: (rec.osm_type as string) ?? '',
osmId: Number(rec.osm_id ?? 0),
category: (rec.category as string) ?? '',
kind: (rec.type as string) ?? ''
};
});
} catch (err) {
lastErr = err;
}
}
throw lastErr ?? new Error('Nominatim unavailable');
}

View File

@ -21,7 +21,7 @@ export interface Searchable {
id: string; id: string;
name: string; name: string;
label: string; // human label in results label: string; // human label in results
kind: 'poi' | 'zone'; kind: 'poi' | 'zone' | 'path';
layerName: string; layerName: string;
layerLabel: string; layerLabel: string;
layerColor: string; layerColor: string;

View File

@ -9,7 +9,7 @@
36.3, 36.3,
-95.7 -95.7
], ],
"featureCount": 462, "featureCount": 512,
"path": "/data/tulsa.geojson" "path": "/data/tulsa.geojson"
}, },
{ {
@ -29,5 +29,23 @@
"color": "#0ea5e9", "color": "#0ea5e9",
"featureCount": 2, "featureCount": 2,
"path": "/data/custom/districts.geojson" "path": "/data/custom/districts.geojson"
},
{
"name": "trails",
"label": "Trails & Routes",
"category": "custom",
"layerType": "path",
"color": "#22c55e",
"featureCount": 1,
"path": "/data/custom/trails.geojson"
},
{
"name": "rogers-county",
"label": "Rogers County",
"category": "custom",
"layerType": "poi",
"color": "#f59e0b",
"featureCount": 7,
"path": "/data/custom/rogers-county.geojson"
} }
] ]

View File

@ -0,0 +1 @@
{"type":"FeatureCollection","crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},"features":[{"type":"Feature","id":"custom/rogers-county/Claremore Museum of History","properties":{"name":"Claremore Museum of History","description":"Local museum","address":"121 N Weenonah Ave, Claremore, OK 74017"},"geometry":{"type":"Point","coordinates":[-95.6112936,36.3114856]}},{"type":"Feature","id":"custom/rogers-county/Rogers County Courthouse","properties":{"name":"Rogers County Courthouse","description":"County seat"},"geometry":{"type":"Point","coordinates":[-95.616414,36.3110143]}},{"type":"Feature","id":"custom/rogers-county/Claremore Indian Hospital","properties":{"name":"Claremore Indian Hospital","description":"Health center"},"geometry":{"type":"Point","coordinates":[-95.6292597,36.3162771]}},{"type":"Feature","id":"custom/rogers-county/Rogers State University","properties":{"name":"Rogers State University","description":"University","address":"1701 W Will Rogers Blvd, Claremore, OK 74017"},"geometry":{"type":"Point","coordinates":[-95.6361137,36.3186099]}},{"type":"Feature","id":"custom/rogers-county/Lake Claremore","properties":{"name":"Lake Claremore"},"geometry":{"type":"Point","coordinates":[-95.5649169,36.3414586]}},{"type":"Feature","id":"custom/rogers-county/Oologah","properties":{"name":"Oologah","description":"Town"},"geometry":{"type":"Point","coordinates":[-95.7083151,36.4470387]}},{"type":"Feature","id":"custom/rogers-county/Verdigris","properties":{"name":"Verdigris","description":"Town"},"geometry":{"type":"Point","coordinates":[-95.6910927,36.2348197]}}]}

View File

@ -0,0 +1 @@
{"type":"FeatureCollection","crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},"features":[{"type":"Feature","id":"custom/trails/Claremore Lake Loop","properties":{"name":"Claremore Lake Loop","description":"Example walking route around the lake"},"geometry":{"type":"LineString","coordinates":[[-95.5649,36.3415],[-95.57,36.343],[-95.575,36.342],[-95.572,36.336],[-95.566,36.3365]]}}]}

File diff suppressed because one or more lines are too long