267 lines
7.1 KiB
JavaScript
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);
|
|
};
|