beerbuddy/openlayers/main.js
2023-12-11 18:44:38 -08:00

267 lines
7.1 KiB
JavaScript

import "./style.css";
import { Map, View, Overlay } from "ol";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import { TileJSON, Vector as VectorSource } from "ol/source";
import { useGeographic } from "ol/proj.js";
import { Control, defaults as defaultControls } from "ol/control.js";
import Search from "ol-ext/control/Search.js";
import { Popover } from "bootstrap";
import utmObj from "utm-latlng";
import makePin from "./modules/makePin.js";
// utm converter
const utm = new utmObj();
// specify to use real lon/lat coordinates
useGeographic();
// state bool for locking pin overlay while adding pin
let pinning = false;
// create new VectorSource for pins
const pinSource = new VectorSource();
// create vector layer for rendering pins and popups
const pinLayer = new VectorLayer({
source: pinSource,
});
// pull data from flask
fetch(import.meta.env.VITE_BACKEND_URL)
.then((res) => res.json())
.then((dbPins) => {
dbPins.forEach((dbPin) => {
// convert to lat/lon
const [lat, lon] = [
utm.convertUtmToLatLng(
dbPin.easting,
dbPin.northing,
dbPin.zone,
dbPin.zoneLetter
).lat,
utm.convertUtmToLatLng(
dbPin.easting,
dbPin.northing,
dbPin.zone,
dbPin.zoneLetter
).lng,
];
// make pin feature
const pin = makePin(
lon.toFixed(5),
lat.toFixed(5),
dbPin.name,
dbPin.cheapestItem,
dbPin.cheapestFloz
);
// add
pinSource.addFeature(pin);
});
});
// create button
// made in global scope so it can be modified
const button = document.createElement("button");
button.textContent = "New";
button.addEventListener("click", newButtonHandler, false);
function newButtonHandler() {
window.ReactNativeWebView?.postMessage("new pin start");
}
// add button to control
class ButtonControl extends Control {
constructor(opt_options) {
const options = opt_options || {};
const element = document.createElement("div");
element.className = "new-button ol-unselectable ol-control";
element.appendChild(button);
super({
element: element,
target: options.target,
});
}
}
// create search bar
const search = new Search({
// disable autocompletion
typing: -1,
placeholder: "Search for products...",
collapsed: "false",
className: "searchBar",
});
// create OSM map
const map = new Map({
controls: defaultControls().extend([new ButtonControl()]),
target: document.getElementById("map"),
layers: [
new TileLayer({
source: new TileJSON({
url: `https://api.maptiler.com/maps/hybrid/tiles.json?key=${
import.meta.env.VITE_MAPTILER_KEY
}`,
tileSize: 512,
crossOrigin: "anonymous",
}),
}),
],
// initially view at Null Point, 2x zoom
view: new View({
center: [0, 0],
zoom: 2,
maxZoom: 20,
}),
});
// popup element to display when pin clicked
const element = document.getElementById("popup");
const popup = new Overlay({
element: element,
positioning: "bottom-center",
stopEvent: false,
});
// add popup overlay to map
map.addOverlay(popup);
// add search
map.addControl(search);
// get search input field
const searchInput = search.getInputField();
searchInput.onkeydown = (event) => {
if (event.key === "Enter") {
// emits to RN
window.ReactNativeWebView?.postMessage(
`search@${map.getView().getCenter()}:${event.target.value}`
);
}
};
// set state for popup visibility
let popover;
const disposePopover = () => {
if (popover) {
popover.dispose();
popover = undefined;
}
};
// on click events
map.on("click", (event) => {
// get features at pixel
const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => feature);
// preemptively remove any previous popup overlays
disposePopover();
// if no pins at location, do nothing
if (!feature) return;
// set popup window position
popup.setPosition(event.coordinate);
// create popover at position
popover = new Popover(element, {
placement: "top",
html: true,
title: feature.get("store"),
content: function () {
const holder = document.createElement("div");
const one = document.createElement("div");
one.textContent = `Cheapest Item: ${feature.get("cheapestItem")}`;
holder.appendChild(one);
const two = document.createElement("div");
two.textContent = `$ / oz.: ${feature.get("pricePerOz")}`;
holder.appendChild(two);
const link = document.createElement("a");
link.href = "#";
link.onclick = function (event) {
event.preventDefault();
window.ReactNativeWebView?.postMessage(
`open@${feature.getGeometry().getCoordinates()}`
);
};
link.textContent = "More...";
holder.appendChild(link);
return holder;
},
});
// show popover
popover.show();
});
// set pointer to cursor when hovering over pin
map.on("pointermove", (event) => {
// if there is a pin at the pixel the user is hovering over
if (map.hasFeatureAtPixel(map.getEventPixel(event.originalEvent))) {
// set the cursor to pointer cursor
map.getTarget().style.cursor = "pointer";
}
// otherwise make cursor normal again
else map.getTarget().style.cursor = "";
});
// close popup when map moved
map.on("movestart", disposePopover);
// sets listener to listen for zoom changes
map.getView().on("change:resolution", () => {
map.getView().getZoom() < 5
? map.getLayers().removeAt(1)
: (() => {
if (map.getLayers().getLength() === 1) {
if (!pinning) map.getLayers().extend([pinLayer]);
}
})();
});
// centers location on user based on location information passed in from expo-location
window.passLocation = (lon, lat) => {
const view = new View({
center: [lon, lat],
zoom: 18,
maxZoom: 20,
});
map.setView(view);
map.getLayers().extend([pinLayer]);
// sets NEW listener to listen for zoom changes
view.on("change:resolution", () => {
view.getZoom() < 5
? map.getLayers().removeAt(1)
: (() => {
if (map.getLayers().getLength() === 1) {
if (!pinning) map.getLayers().extend([pinLayer]);
}
})();
});
};
// place pin interaction
window.placePin = () => {
// removes listener on button in case someone wants to mash it
button.removeEventListener("click", newButtonHandler, false);
// removes pin layer
pinning = true;
map.getLayers().removeAt(1);
// adds pin in center of view
document.getElementById("centerPin").style.display = "block";
// changes new button to OK button
button.textContent = "OK";
// OK button has event listener that on click
function OKButtonHandler() {
// removes listener on button in case someone wants to mash it
button.removeEventListener("click", OKButtonHandler, false);
// removes pin in center of view
document.getElementById("centerPin").style.display = "none";
// emits to RN
window.ReactNativeWebView?.postMessage(
`create@${map.getView().getCenter()}`
);
}
button.addEventListener("click", OKButtonHandler, false);
};