diff --git a/demo/proximity-maps.html b/demo/proximity-maps.html index 93d63d8..5574142 100644 --- a/demo/proximity-maps.html +++ b/demo/proximity-maps.html @@ -27,6 +27,7 @@ } geo-wiki { + display: block; background: white; border: solid 2px black; border-radius: 5px; @@ -45,6 +46,13 @@ pointer-events: none; background-color: #b4d8f63b; } + + folk-weather { + display: block; + background: white; + border: solid 2px black; + border-radius: 5px; + } @@ -53,24 +61,30 @@ - + - + + + + + diff --git a/src/folk-hull.ts b/src/folk-hull.ts index 2dec53a..4de0371 100644 --- a/src/folk-hull.ts +++ b/src/folk-hull.ts @@ -1,6 +1,12 @@ import { FolkSet } from './folk-set'; import { Vertex, verticesToPolygon } from './arrows/utils'; +declare global { + interface HTMLElementTagNameMap { + 'folk-hull': FolkHull; + } +} + export class FolkHull extends FolkSet { static tagName = 'folk-hull'; diff --git a/src/folk-proximity.ts b/src/folk-proximity.ts new file mode 100644 index 0000000..c3e8c1c --- /dev/null +++ b/src/folk-proximity.ts @@ -0,0 +1,119 @@ +import { collisionDetection } from './collision'; +import { FolkHull } from './folk-hull'; +import { FolkGeometry } from './canvas/fc-geometry.ts'; + +// TODO dont hard code this +const PROXIMITY = 50; + +declare global { + interface HTMLElementTagNameMap { + 'folk-cluster': FolkCluster; + 'folk-proximity': FolkProximity; + } +} + +export class FolkCluster extends FolkHull { + static tagName = 'folk-cluster'; + + #data = new Map(); + + isElementInCluster(element: FolkGeometry) { + return this.sourceElements.includes(element); + } + + isElementInProximity(element: FolkGeometry) { + for (const el of this.sourceElements as FolkGeometry[]) { + if (collisionDetection(el.getClientRect(), element.getClientRect(), PROXIMITY)) return true; + } + return false; + } + + addElements(...elements) { + this.sources = this.sourceElements + .concat(elements) + .map((el) => `#${el.id}`) + .join(', '); + } + + removeElement(element) { + this.sources = this.sourceElements + .filter((el) => el !== element) + .map((el) => `#${el.id}`) + .join(', '); + } +} + +export class FolkProximity extends HTMLElement { + static tagName = 'folk-proximity'; + + static register() { + customElements.define(this.tagName, this); + } + + #clusters = new Set(); + #geometries = Array.from(this.querySelectorAll('fc-geometry')); + + constructor() { + super(); + + this.addEventListener('move', this.#handleProximity); + this.addEventListener('resize', this.#handleProximity); + // document.addEventListener('recenter', (e) => { + // proximityMap.get(e.target.parentElement)?.forEach((el) => { + // const content = el.firstElementChild; + // if (content instanceof GeoWiki) { + // const { lat, lng } = e.target.coordinates; + // content.coordinates = [lat, lng]; + // } + // }); + // }); + } + + #handleProximity = (e) => { + const el = e.target as FolkGeometry; + + const cluster = this.#findCluster(el); + + if (cluster === null) { + for (const cluster of this.#clusters) { + // what if its in proximity to multiple clusters? + if (cluster.isElementInProximity(el)) { + cluster.addElements(el); + return; + } + } + + for (const geometry of this.#geometries) { + if (geometry === el) break; + + if (collisionDetection(geometry.getClientRect(), el.getClientRect(), PROXIMITY)) { + const cluster = document.createElement('folk-cluster'); + cluster.addElements(geometry, el); + this.#clusters.add(cluster); + this.appendChild(cluster); + return; + } + } + } else { + const isInCluster = (cluster.sourceElements as FolkGeometry[]) + .filter((element) => el !== element) + .some((element) => collisionDetection(el.getClientRect(), element.getClientRect(), PROXIMITY)); + + if (!isInCluster) { + cluster.removeElement(el); + + if (cluster.sourcesMap.size === 1) { + this.#clusters.delete(cluster); + cluster.remove(); + } + } + } + }; + + #findCluster(element) { + for (const cluster of this.#clusters) { + if (cluster.isElementInCluster(element)) return cluster; + } + return null; + } +} diff --git a/src/folk-set.ts b/src/folk-set.ts index 0321101..582055f 100644 --- a/src/folk-set.ts +++ b/src/folk-set.ts @@ -2,6 +2,12 @@ import { ClientRectObserverEntry, ClientRectObserverManager } from './client-rec const clientRectObserver = new ClientRectObserverManager(); +declare global { + interface HTMLElementTagNameMap { + 'folk-set': FolkSet; + } +} + export class FolkSet extends HTMLElement { static tagName = 'folk-set'; diff --git a/src/folk-weather.ts b/src/folk-weather.ts new file mode 100644 index 0000000..54b30dd --- /dev/null +++ b/src/folk-weather.ts @@ -0,0 +1,68 @@ +interface Weather { + temperature: string; + windSpeed: string; +} + +declare global { + interface HTMLElementTagNameMap { + 'folk-weather': FolkWeather; + } +} + +export class FolkWeather extends HTMLElement { + static tagName = 'folk-weather'; + + static register() { + customElements.define(this.tagName, this); + } + + static observedAttributes = ['coordinates']; + + #coordinates = [0, 0] as const; + #results: Weather | null = null; + + get coordinates() { + return this.#coordinates; + } + + set coordinates(coordinates) { + this.setAttribute('coordinates', coordinates.join(', ')); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'coordinates') { + this.#coordinates = newValue.split(',').map((str) => Number(str)) || [0, 0]; + this.fetchWeather(this.#coordinates); + } + } + + async fetchWeather([lat, long]: readonly [number, number]) { + const params = new URLSearchParams({ + latitude: lat.toString(), + longitude: long.toString(), + current: 'temperature_2m,wind_speed_10m', + temperature_unit: 'fahrenheit', + wind_speed_unit: 'mph', + }); + // https://www.mediawiki.org/wiki/API:Geosearch + this.#results = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`) + .then((response) => response.json()) + .then(({ current, current_units }) => ({ + temperature: `${current.temperature_2m} ${current_units.temperature_2m}`, + windSpeed: `${current.wind_speed_10m} ${current_units.wind_speed_10m}`, + })); + + this.#renderResults(); + } + + #renderResults() { + if (this.#results === null) { + this.innerHTML = ''; + return; + } + this.innerHTML = ` +

Temperature: ${this.#results.temperature}

+

Wind Speed: ${this.#results.windSpeed}

+ `; + } +}