Public
Edited
Dec 10, 2022
2 forks
11 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
osmFetch.value
Insert cell
osmFetch.context
Insert cell
viewof osmFetch = {
const input = Inputs.input();
const machine = osmFetchMachineCreator(
{},
{
actions: {
processSearchResults: xstate.assign({
location: (context, event) => event.data[0]
}),
resetSearchResults: xstate.assign({ location: undefined }),
addQueryParams: xstate.assign({
queryParams: (context, event) => event.data
}),
processGeoJson: xstate.assign({
geojson: (context, event) => event.data
}),
resetGeoJson: xstate.assign({ geojson: undefined })
},
guards: {
hasLocation: (context, event) => event.data?.[0]?.display_name,
isQueryValid: (context, event) => generateQueryTemplate(event.data),
havePlaceAndValidQuery: (context, event) =>
context?.location?.display_name &&
generateQueryTemplate(context.queryParams),
hasData: (context, event) => event.data && event.data?.features?.length
},
services: {
searchLocation: (context, event) => searchOsm(event.data),
fetchGeojson: (context, event) =>
fetchGeoJson(context.location, context.queryParams)
}
}
);

const service = xstate
.interpret(machine)
.onTransition((state) => {
input.value = state;
input.dispatchEvent(new Event("input", { bubbles: true }));
})
.start();

return Object.assign(input, { service });
}
Insert cell
{
viewof osmFetch.service.send("SEARCH", { data: location });
}
Insert cell
{
viewof osmFetch.service.send("QUERY_CHANGED", { data: query });
}
Insert cell
function searchOsm(query) {
const url = generateOsmSearchEndPoint(query);
return fetch(url).then((res) => res.json());
}
Insert cell
generateOsmSearchEndPoint = (q) => {
const query = encodeURIComponent(q);
return `https://nominatim.openstreetmap.org/search?format=json&q=${query}`;
}
Insert cell
function generateQueryTemplate(query, location) {
return ow(`${query} in "${location}"`, {
timeout: 180,
comment: false,
globalBbox: false,
compactNWR: true
});
}
Insert cell
generateOsmId = (location) => location && `${location.osm_id}`.padStart(8, "0")
Insert cell
function generateQuery(template,location, id) {
return template.replaceAll(
`{{geocodeArea:${location}}}`,
`area(id:36${id})`
);
}
Insert cell
function fetchGeoJson(location, queryParams) {
const osmId = generateOsmId(location);

if (osmId == null) throw new Error("Location data is not valid");

const place = location.display_name;

const template = generateQueryTemplate(queryParams, place);

if (!template) throw new Error("Unable to generate query");

const osmQuery = generateQuery(template, place, osmId);

return overpassQuery(osmQuery);
}
Insert cell
function overpassQuery(query, options) {
return new Promise((resolve, reject) => {
OverpassQuery(
query,
(error, data) => {
if (error) return reject(error);

resolve(data);
},
options
);
});
}
Insert cell
generator = removeQueryFromUrl(document.baseURI) //"https://observablehq.com/@saneef/geojson-from-openstreetmap"
Insert cell
function removeQueryFromUrl(url) {
const urlObj = new URL(url);

urlObj.search = "";

return urlObj.href;
}
Insert cell
function osmFetchMachineCreator(context = {}, options) {
return xstate.createMachine({ ...osmFetchMachineSpec, context }, options);
}
Insert cell
// https://stately.ai/registry/editor/share/4b5bf8ed-7395-43ac-ad12-20116bcdb51b
osmFetchMachineSpec = ({
id: "fetch-osm",
initial: "idle",
states: {
idle: {
type: "parallel",
states: {
search: {
initial: "found",
states: {
found: {},
notFound: {}
}
},
query: {
initial: "valid",
states: {
valid: {},
invalid: {}
}
}
},
on: {
FETCH: {
cond: "havePlaceAndValidQuery",
target: "fetchGeojson"
}
}
},
download: {
initial: "noData",
states: {
ready: {
exit: "resetGeojson",
entry: "processGeoJson"
},
noData: {},
error: {
on: {
FETCH: {
cond: "havePlaceAndValidQuery",
target: "#fetch-osm.fetchGeojson"
}
}
}
}
},
searchLocation: {
invoke: {
src: "searchLocation",
id: "search-location",
onDone: [
{
actions: "processSearchResults",
cond: "hasLocation",
target: "#fetch-osm.idle.search.found"
},
{
target: "#fetch-osm.idle.search.notFound"
}
],
onError: [
{
actions: "resetSearchResults",
target: "#fetch-osm.idle.search.notFound"
}
]
}
},
fetchGeojson: {
invoke: {
src: "fetchGeojson",
id: "fetch-geojson",
onDone: [
{
cond: "hasData",
target: "#fetch-osm.download.ready"
},
{
target: "#fetch-osm.download.noData"
}
],
onError: [
{
target: "#fetch-osm.download.error"
}
]
}
}
},
on: {
QUERY_CHANGED: [
{
actions: "addQueryParams",
cond: "isQueryValid",
target: ".idle.query.valid"
},
{
target: ".idle.query.invalid"
}
],
SEARCH: {
target: ".searchLocation"
}
}
})
Insert cell
function filterFeaturesByGeomTypes(geojson, geometryTypes) {
if (!geojson || !geojson.features.length) return;

const features = filterFeatures(geojson, (f) =>
geometryTypes.includes(turf.getType(f))
);

return { ...geojson, features };
}
Insert cell
function filterFeatures(geo, fn = (d) => d) {
return turf.featureReduce(
geo,
(acc, curr, index) => {
if (Boolean(fn(curr, index, geo))) {
return [...acc, curr];
}
return acc;
},
[]
);
}
Insert cell
function* showGeoJsonPreview(geo) {
if (!geo || !geo.features.length) return md``;

const w = Math.min(width, 640);
const aspectRatio = 1;

let container = DOM.element("div", {
style: `width:${w}px;height:${w / aspectRatio}px`
});

yield container;

let map = L.map(container);
let osmLayer = L.tileLayer(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
).addTo(map);

let areaLayer = L.geoJson(geo, {
weight: 2,
color: "#432"
})
.bindPopup(function (Layer) {
return Layer.feature.properties.NAME;
})
.addTo(map);

map.fitBounds(areaLayer.getBounds());
}
Insert cell
geometryTypes = [
"Point",
"LineString",
"Polygon",
"MultiPoint",
"MultiLineString",
"MultiPolygon"
]
Insert cell
function highlight(content, {} = {}) {
return htl.html`<span style=${{
boxShadow: "0.0625rem 0.0625rem #fcc419",
padding: "0.125rem 0.5rem",
borderRadius: "0.25rem",
fontWeight: "bold",
backgroundColor: "#ffe066",
textShadow: "1px 1px white",
"-webkit-box-decoration-break": "clone",
"box-decoration-break": "clone"
}}>${content}`;
}
Insert cell
sampleUrls = [
[
"Districts of Kerala, India",
getNotebookUrlWithParams({
location: "Kerala",
query: "boundary=administrative and admin_level=5",
selectGeometryTypes: "MultiPolygon-Polygon"
})
],
[
"Museums in Switzerland",
getNotebookUrlWithParams({
location: "Switzerland",
query: "tourism=museum",
selectGeometryTypes: "Point"
})
],
[
"Parks in Bangalore",
getNotebookUrlWithParams({
location: "Bangalore",
query: "leisure=park or leisure=playground",
selectGeometryTypes: "Point-Polygon-MultiPolygon"
})
]
]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more