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); };