+ 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 { defaults as defaultControls } from "ol/control.js";
+
+import { Popover } from "bootstrap";
+
+import searchBar from "./modules/searchBar.js";
+import fetchPins from "./modules/fetchPins.js";
+import ButtonControl from "./modules/newButton.js";
+import changeResolutionHandler from "./modules/changeResolutionHandler.js";
+import pointerMoveHandler from "./modules/pointerMoveHandler.js";
+import searchInputHander from "./modules/searchInputHandler.js";
+
+///
+
+const search = searchBar;
+
+let pinning = false;
+
+let popover;
+
+/** Vector source for pins
+ * @constant {ol.source.VectorSource} */
+const pinSource = new VectorSource();
+/** Vector layer for pin vector source
+ * @constant {ol.layer.VectorLayer} */
+const pinLayer = new VectorLayer({
+ source: pinSource,
+});
+
+useGeographic();
+
+fetchPins(pinSource);
+
+/** Main map
+ * @constant {ol.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",
+ }),
+ }),
+ ],
+ view: new View({
+ center: [0, 0],
+ zoom: 2,
+ maxZoom: 20,
+ }),
+});
+
+/** Overlay element for pin popups
+ * @constant {ol.Overlay} */
+const popup = new Overlay({
+ element: document.getElementById("popup"),
+ positioning: "bottom-center",
+ stopEvent: false,
+});
+
+map.addOverlay(popup);
+
+map.addControl(search);
+
+const searchInput = search.getInputField();
+searchInput.onkeydown = (event) => {
+ searchInputHander(event, map);
+};
+
+/** Removes store information popup from screen
+ */
+const disposePopover = () => {
+ if (popover) {
+ popover.dispose();
+ popover = undefined;
+ }
+};
+
+/** Handles user clicks. If user clicks on a pixel that contains a pin, display popup with store information
+ * @param {Event} event - User click event
+ * @param {bootstrap.Popover} popover - Popover object from Bootstrap/Popper
+ * @param {ol.Overlay} popup - Overlay element for pin popups
+ * @param {ol.Map} map - OpenLayers map
+ */
+
+map.on("click", (event) => {
+ disposePopover();
+
+ const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => feature);
+ if (!feature) return;
+
+ popup.setPosition(event.coordinate);
+
+ const element = document.querySelector("#popup");
+
+ popover = new Popover(element, {
+ placement: "top",
+ html: true,
+ title: feature.get("store"),
+ content: function () {
+ const container = document.createElement("div");
+
+ const cheapestItem = document.createElement("div");
+ cheapestItem.textContent = `Cheapest Item: ${feature.get(
+ "cheapestItem"
+ )}`;
+ container.appendChild(cheapestItem);
+
+ const cheapestPerFloz = document.createElement("div");
+ cheapestPerFloz.textContent = `$ / oz.: ${feature.get("pricePerOz")}`;
+ container.appendChild(cheapestPerFloz);
+
+ const link = document.createElement("a");
+ link.href = "#";
+ link.onclick = function (event) {
+ event.preventDefault();
+ window.ReactNativeWebView?.postMessage(
+ `open@${feature.getGeometry().getCoordinates()}`
+ );
+ };
+ link.textContent = "More...";
+ container.appendChild(link);
+
+ return container;
+ },
+ });
+
+ popover.show();
+});
+
+map.on("pointermove", (event) => {
+ pointerMoveHandler(event, map);
+});
+
+map.on("movestart", disposePopover);
+
+map.getView().on("change:resolution", () => {
+ changeResolutionHandler(pinning, map, pinLayer);
+});
+
+/** Centers location on user based on location information passed in from expo-location
+ * @param {number} lon - User's longitude
+ * @param {number} lat - User's latitude
+ */
+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]);
+ }
+ })();
+ });
+};
+
+/** Begins store creation process if user clicks on "New" button
+ */
+window.placePin = () => {
+ button.removeEventListener("click", newButtonHandler, false);
+ 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);
+};
+
+
+