progress update 2023-11-29
backend code complete, to be uploaded to deta
This commit is contained in:
parent
f73bc71489
commit
9a9e6ac209
26 changed files with 25338 additions and 0 deletions
5
flask/.gitignore
vendored
Normal file
5
flask/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
__pycache__
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
.space
|
||||||
|
Spacefile
|
||||||
253
flask/backend.py
Normal file
253
flask/backend.py
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
from os import environ
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from flask import Flask, request, abort
|
||||||
|
from datetime import datetime
|
||||||
|
from deta import Deta
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
app = Flask(__name__)
|
||||||
|
deta = Deta()
|
||||||
|
|
||||||
|
### connect to dbs and drive
|
||||||
|
itemsDB = deta.Base('items')
|
||||||
|
storesDB = deta.Base('stores')
|
||||||
|
drive = deta.Drive('beerbuddy')
|
||||||
|
|
||||||
|
### reusable functions
|
||||||
|
|
||||||
|
# fetch requested store from store database based on request arguments
|
||||||
|
# arguments are assumed checked beforehand
|
||||||
|
def fetchStores():
|
||||||
|
# fetch store from deta Base
|
||||||
|
fetch = storesDB.fetch({
|
||||||
|
'easting': request.args['easting'],
|
||||||
|
'northing': request.args['northing'],
|
||||||
|
'zone': request.args['zone'],
|
||||||
|
})
|
||||||
|
data = fetch.items
|
||||||
|
# if we didn't find the record on the first go around, try again
|
||||||
|
if not data:
|
||||||
|
### inb4 infinite loop
|
||||||
|
while (fetch.last is not None):
|
||||||
|
fetch = storesDB.fetch({
|
||||||
|
'easting': request.args['easting'],
|
||||||
|
'northing': request.args['northing'],
|
||||||
|
'zone': request.args['zone'],
|
||||||
|
last: fetch.last
|
||||||
|
})
|
||||||
|
# check with each loop iteration if we've found it
|
||||||
|
if fetch.items:
|
||||||
|
data.append(fetch.items)
|
||||||
|
# if so, break loop
|
||||||
|
break
|
||||||
|
return data
|
||||||
|
|
||||||
|
# fetch requested item from item database based on request arguments and passed store
|
||||||
|
def fetchItems(store):
|
||||||
|
# fetch item from deta Base
|
||||||
|
fetch = itemsDB.fetch({
|
||||||
|
'name': request.args['itemName'],
|
||||||
|
'store': store['key'],
|
||||||
|
})
|
||||||
|
data = fetch.items
|
||||||
|
# if we didn't find the record on the first go around, try again
|
||||||
|
if not data:
|
||||||
|
### inb4 infinite loop
|
||||||
|
while (fetch.last is not None):
|
||||||
|
fetch = itemsDB.fetch({
|
||||||
|
'name': request.args['itemName'],
|
||||||
|
'store': store['key'],
|
||||||
|
last: fetch.last
|
||||||
|
})
|
||||||
|
# check with each loop iteration if we've found it
|
||||||
|
if fetch.items:
|
||||||
|
data.append(fetch.items)
|
||||||
|
# if so, break loop
|
||||||
|
break
|
||||||
|
return data
|
||||||
|
|
||||||
|
# fetches all items from item database
|
||||||
|
def fetchAllItems(store):
|
||||||
|
# fetch item from deta Base
|
||||||
|
fetch = itemsDB.fetch({
|
||||||
|
'store': store['key'],
|
||||||
|
})
|
||||||
|
data = fetch.items
|
||||||
|
# if we didn't find the record on the first go around, try again
|
||||||
|
if not data:
|
||||||
|
### inb4 infinite loop
|
||||||
|
while (fetch.last is not None):
|
||||||
|
fetch = itemsDB.fetch({
|
||||||
|
'store': store['key'],
|
||||||
|
last: fetch.last
|
||||||
|
})
|
||||||
|
data.append(fetch.items)
|
||||||
|
return data
|
||||||
|
|
||||||
|
# compares this item to other items from its store to determine if it is the cheapest then updates store accordingly
|
||||||
|
def updateCheapest(item, store):
|
||||||
|
cheapest = true
|
||||||
|
storeItems = fetchAllItems(store)
|
||||||
|
for storeItem in storeItems:
|
||||||
|
if storeItem['perFloz'] < item['perFloz']: cheapest = false
|
||||||
|
if cheapest:
|
||||||
|
store['cheapestItem'] = item['name']
|
||||||
|
store['cheapestFloz'] = item['perFloz']
|
||||||
|
storesDB.put(store)
|
||||||
|
|
||||||
|
### routes
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
def get():
|
||||||
|
# if there are no arguments on the request
|
||||||
|
if not request.args:
|
||||||
|
# get all stores
|
||||||
|
fetch = storesDB.fetch({})
|
||||||
|
data = fetch.items
|
||||||
|
### inb4 infinite loop
|
||||||
|
while (fetch.last is not None):
|
||||||
|
fetch = storesDB.fetch({
|
||||||
|
last: fetch.last
|
||||||
|
})
|
||||||
|
data.append(fetch.items)
|
||||||
|
else:
|
||||||
|
# get store location details from URL arguments
|
||||||
|
if request.args['easting'] and request.args['northing'] and request.args['zone']:
|
||||||
|
stores = fetchStores()
|
||||||
|
if not stores: abort(404)
|
||||||
|
store = stores[0]
|
||||||
|
# if an item GET request
|
||||||
|
if request.args['itemName']:
|
||||||
|
# passing through the store at position [0] in the fetched stores array
|
||||||
|
items = fetchItems(store)
|
||||||
|
if not items: abort(404)
|
||||||
|
data = items[0]
|
||||||
|
# otherwise is a store GET request
|
||||||
|
else: data = store
|
||||||
|
# if wrong arguments, malformed request
|
||||||
|
else: abort(400)
|
||||||
|
# otherwise return data
|
||||||
|
return data
|
||||||
|
|
||||||
|
@app.route('/', methods=['POST'])
|
||||||
|
def post():
|
||||||
|
# checks for arguments
|
||||||
|
if request.args:
|
||||||
|
if request.args['easting'] and request.args['northing'] and request.args['zone'] and request.args['itemName'] and request.args['itemPrice'] and request.args['itemVolume']:
|
||||||
|
# checks if this is posting a new store
|
||||||
|
if request.args['name']:
|
||||||
|
# test for presence of stores in close vicinity with similar names
|
||||||
|
nameSubstrings = request.args['name'].split()
|
||||||
|
for nameSubstring in nameSubstrings:
|
||||||
|
fetch = storesDB.fetch({
|
||||||
|
'easting?r': [request.args['easting'] - 10, request.args['easting'] + 10],
|
||||||
|
'northing?r': [request.args['northing'] - 10, request.args['northing'] + 10],
|
||||||
|
'zone': request.args['zone'],
|
||||||
|
'name?contains': nameSubstring,
|
||||||
|
})
|
||||||
|
stores = fetch.items
|
||||||
|
# if we didn't find the record on the first go around, try again
|
||||||
|
if not stores:
|
||||||
|
### inb4 infinite loop
|
||||||
|
while (fetch.last is not None):
|
||||||
|
fetch = storesDB.fetch({
|
||||||
|
'easting?r': [request.args['easting'] - 10, request.args['easting'] + 10],
|
||||||
|
'northing?r': [request.args['northing'] - 10, request.args['northing'] + 10],
|
||||||
|
'zone': request.args['zone'],
|
||||||
|
'name?contains': 'nameSubstring',
|
||||||
|
last: fetch.last
|
||||||
|
})
|
||||||
|
# check with each loop iteration if we've found it
|
||||||
|
if fetch.items:
|
||||||
|
stores.append(fetch.items)
|
||||||
|
# if so, break loop
|
||||||
|
break
|
||||||
|
# if found a store within +/-10 meters named similarly to one of the substrings, abort
|
||||||
|
if stores: abort(409)
|
||||||
|
# by this point, app would have aborted if there were a problem
|
||||||
|
# let's create the records
|
||||||
|
# first the item dict
|
||||||
|
item = {
|
||||||
|
'name': request.args['itemName'],
|
||||||
|
'volumeFloz': request.args['itemVolume'],
|
||||||
|
'price': request.args['itemPrice'],
|
||||||
|
'perFloz': request.args['itemPrice'] / request.args['itemVolume'],
|
||||||
|
'lastUpdated': datetime.now().strftime('%m-%d-%Y %H:%M:%S'),
|
||||||
|
}
|
||||||
|
# upload store to DB
|
||||||
|
store = storesDB.put({
|
||||||
|
'easting': request.args['easting'],
|
||||||
|
'northing': request.args['northing'],
|
||||||
|
'zone': request.args['zone'],
|
||||||
|
'name': request.args['name'],
|
||||||
|
'cheapestItem': item['name'],
|
||||||
|
'cheapestFloz': item['perFloz'],
|
||||||
|
'lastUpdated': datetime.now().strftime('%m-%d-%Y %H:%M:%S'),
|
||||||
|
})
|
||||||
|
# upload item to DB
|
||||||
|
item['store'] = store['key']
|
||||||
|
itemsDB.put(item)
|
||||||
|
# if there is an image upload
|
||||||
|
if request.args['image']:
|
||||||
|
drive.put(store.key, data=request.args['image'])
|
||||||
|
# finish successfully
|
||||||
|
return {}
|
||||||
|
# or if it's posting a new item in the store
|
||||||
|
else:
|
||||||
|
stores = fetchStores()
|
||||||
|
if not stores: abort(404)
|
||||||
|
store = stores[0]
|
||||||
|
###
|
||||||
|
### HERE WE NEED TO CHECK IF ITEM ALREADY EXISTS
|
||||||
|
###
|
||||||
|
# let's create the record
|
||||||
|
item = {
|
||||||
|
'name': request.args['itemName'],
|
||||||
|
'volumeFloz': request.args['itemVolume'],
|
||||||
|
'price': request.args['itemPrice'],
|
||||||
|
'perFloz': request.args['itemPrice'] / request.args['itemVolume'],
|
||||||
|
'lastUpdated': datetime.now().strftime('%m-%d-%Y %H:%M:%S'),
|
||||||
|
'store': store['key']
|
||||||
|
}
|
||||||
|
updateCheapest(item, store)
|
||||||
|
# beam it up
|
||||||
|
itemsDB.put(item)
|
||||||
|
# finish successfully
|
||||||
|
return {}
|
||||||
|
# aborts if conditional chains not met
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
@app.route('/', methods=['PUT'])
|
||||||
|
def put():
|
||||||
|
# checks for arguments
|
||||||
|
if request.args:
|
||||||
|
if request.args['easting'] and request.args['northing'] and request.args['zone']:
|
||||||
|
# find store
|
||||||
|
stores = fetchStores()
|
||||||
|
if not stores: abort(404)
|
||||||
|
store = stores[0]
|
||||||
|
# updating item price
|
||||||
|
if request.args['itemName'] and request.args['price']:
|
||||||
|
# find item
|
||||||
|
items = fetchItems(store)
|
||||||
|
if not items: abort (404)
|
||||||
|
item = items[0]
|
||||||
|
# change price
|
||||||
|
item['price'] = request.args['itemPrice']
|
||||||
|
item['perFloz'] = item['price'] / item['volumeFloz']
|
||||||
|
item['lastUpdated'] = datetime.now().strftime('%m-%d-%Y %H:%M:%S')
|
||||||
|
updateCheapest(item, store)
|
||||||
|
# beam it up
|
||||||
|
itemsDB.put(item)
|
||||||
|
# finish successfully
|
||||||
|
return {}
|
||||||
|
# updating store image
|
||||||
|
if request.args['image']:
|
||||||
|
store['lastUpdated'] = datetime.now().strftime('%m-%d-%Y %H:%M:%S')
|
||||||
|
storesDB.put(store)
|
||||||
|
drive.put(store.key, data=request.args['image'])
|
||||||
|
return {}
|
||||||
|
# aborts if conditional chains not met
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
# no deletion planned (for now)
|
||||||
3
openlayers/.gitignore
vendored
Normal file
3
openlayers/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
46
openlayers/index.html
Normal file
46
openlayers/index.html
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
|
||||||
|
/>
|
||||||
|
<title>Using OpenLayers with Vite</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map">
|
||||||
|
<div id="centerPin">
|
||||||
|
<svg
|
||||||
|
width="32px"
|
||||||
|
height="32px"
|
||||||
|
viewBox="-0.12 -0.12 12.24 12.24"
|
||||||
|
enable-background="new 0 0 12 12"
|
||||||
|
id="Слой_1"
|
||||||
|
version="1.1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
fill="#000000"
|
||||||
|
stroke="#000000"
|
||||||
|
stroke-width="0.12000000000000002"
|
||||||
|
>
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||||
|
<g
|
||||||
|
id="SVGRepo_tracerCarrier"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<path
|
||||||
|
d="M6,0C3.2385864,0,1,2.2385864,1,5s2.5,5,5,7c2.5-2,5-4.2385864,5-7S8.7614136,0,6,0z M6,7 C4.8954468,7,4,6.1045532,4,5s0.8954468-2,2-2s2,0.8954468,2,2S7.1045532,7,6,7z"
|
||||||
|
fill="#ed333b"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="popup"></div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
225
openlayers/main.js
Normal file
225
openlayers/main.js
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import { Map, View, Overlay, Feature } from "ol";
|
||||||
|
import { Icon, Style } from "ol/style";
|
||||||
|
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 Point from "ol/geom/Point.js";
|
||||||
|
import { Popover } from "bootstrap";
|
||||||
|
|
||||||
|
// specify to use real lon/lat coordinates
|
||||||
|
useGeographic();
|
||||||
|
|
||||||
|
// state bool for locking pin overlay while adding pin
|
||||||
|
let pinning = false;
|
||||||
|
|
||||||
|
// creates pins
|
||||||
|
const makePin = (lon, lat, storeName, cheapest, flozPrice) => {
|
||||||
|
// define pin graphics (since this is all inline)
|
||||||
|
const pinSVG = `<svg width="32px" height="32px" viewBox="-0.12 -0.12 12.24 12.24" enable-background="new 0 0 12 12" id="Слой_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" stroke="#000000" stroke-width="0.12000000000000002"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M6,0C3.2385864,0,1,2.2385864,1,5s2.5,5,5,7c2.5-2,5-4.2385864,5-7S8.7614136,0,6,0z M6,7 C4.8954468,7,4,6.1045532,4,5s0.8954468-2,2-2s2,0.8954468,2,2S7.1045532,7,6,7z" fill="#ed333b"></path></g></svg>`;
|
||||||
|
const pinSVGBlob = new Blob([pinSVG], {
|
||||||
|
type: "image/svg+xml",
|
||||||
|
});
|
||||||
|
const pinImageURL = URL.createObjectURL(pinSVGBlob);
|
||||||
|
|
||||||
|
// define style for all pins
|
||||||
|
const pinStyle = new Style({
|
||||||
|
image: new Icon({
|
||||||
|
src: pinImageURL,
|
||||||
|
anchor: [0.5, 1],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// create pin as feature
|
||||||
|
const pin = new Feature({
|
||||||
|
geometry: new Point([lon, lat]),
|
||||||
|
store: storeName,
|
||||||
|
cheapestItem: cheapest,
|
||||||
|
pricePerOz: flozPrice,
|
||||||
|
});
|
||||||
|
|
||||||
|
// set style for pin
|
||||||
|
pin.setStyle(pinStyle);
|
||||||
|
|
||||||
|
return pin;
|
||||||
|
};
|
||||||
|
|
||||||
|
// create new VectorSource for pins
|
||||||
|
const pinSource = new VectorSource();
|
||||||
|
|
||||||
|
// FLASK PULL LOOP
|
||||||
|
|
||||||
|
// test
|
||||||
|
const pin = makePin(0, 0, "Null Store", "Modelo", "0.30");
|
||||||
|
|
||||||
|
// add pin
|
||||||
|
pinSource.addFeature(pin);
|
||||||
|
|
||||||
|
// END FLASK PULL LOOP
|
||||||
|
|
||||||
|
// create vector layer for rendering pins and popups
|
||||||
|
const pinLayer = new VectorLayer({
|
||||||
|
source: pinSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 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=iE1hOruK6f8SDwxrEIir",
|
||||||
|
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);
|
||||||
|
|
||||||
|
// set state for popup visibility
|
||||||
|
let popover;
|
||||||
|
const disposePopover = () => {
|
||||||
|
if (popover) {
|
||||||
|
popover.dispose();
|
||||||
|
popover = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// on click events
|
||||||
|
map.on("click", (event) => {
|
||||||
|
console.log(event.coordinate);
|
||||||
|
// 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: `Cheapest Item: ${feature.get(
|
||||||
|
"cheapestItem"
|
||||||
|
)}<br/>$ / oz.: $0.30<br/><a href=''>More...</a><br/>`,
|
||||||
|
});
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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(`@${map.getView().getCenter()}`);
|
||||||
|
}
|
||||||
|
button.addEventListener("click", OKButtonHandler, false);
|
||||||
|
};
|
||||||
1452
openlayers/package-lock.json
generated
Normal file
1452
openlayers/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
openlayers/package.json
Normal file
18
openlayers/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "beerbuddy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.2",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"ol": "latest",
|
||||||
|
"vite-plugin-singlefile": "^0.13.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
openlayers/readme.md
Normal file
4
openlayers/readme.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# OpenLayers + Vite
|
||||||
|
|
||||||
|
First stage of the app is the creation of an OpenLayers app that is inlined into a single file by Vite.
|
||||||
|
This is then an asset that can be interacted with from within React Native.
|
||||||
44
openlayers/style.css
Normal file
44
openlayers/style.css
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
@import "node_modules/ol/ol.css";
|
||||||
|
@import "node_modules/bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.popover {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-button {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 1.25em;
|
||||||
|
width: fit-content;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-button > button {
|
||||||
|
padding: 1.25em;
|
||||||
|
width: fit-content;
|
||||||
|
line-height: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#centerPin {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 50%;
|
||||||
|
right: calc(50% - 16px);
|
||||||
|
z-index: 1;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#centerPin > svg {
|
||||||
|
left: -25%;
|
||||||
|
}
|
||||||
5
openlayers/vite.config.js
Normal file
5
openlayers/vite.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [viteSingleFile()],
|
||||||
|
};
|
||||||
35
react-native/.gitignore
vendored
Normal file
35
react-native/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
web-build/
|
||||||
|
|
||||||
|
# Native
|
||||||
|
*.orig.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.*
|
||||||
|
yarn-debug.*
|
||||||
|
yarn-error.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
22
react-native/App.tsx
Normal file
22
react-native/App.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import Map from "./components/Map";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<SafeAreaProvider>
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<Map />
|
||||||
|
</SafeAreaView>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
258
react-native/UTM_converter.ts
Normal file
258
react-native/UTM_converter.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
// taken from https://www.npmjs.com/package/utm-projection due to expo/npm glitch
|
||||||
|
// all rights aliothor
|
||||||
|
|
||||||
|
var K0 = 0.9996;
|
||||||
|
|
||||||
|
var E = 0.00669438;
|
||||||
|
var E2 = Math.pow(E, 2);
|
||||||
|
var E3 = Math.pow(E, 3);
|
||||||
|
var E_P2 = E / (1 - E);
|
||||||
|
|
||||||
|
var SQRT_E = Math.sqrt(1 - E);
|
||||||
|
var _E = (1 - SQRT_E) / (1 + SQRT_E);
|
||||||
|
var _E2 = Math.pow(_E, 2);
|
||||||
|
var _E3 = Math.pow(_E, 3);
|
||||||
|
var _E4 = Math.pow(_E, 4);
|
||||||
|
var _E5 = Math.pow(_E, 5);
|
||||||
|
|
||||||
|
var M1 = 1 - E / 4 - (3 * E2) / 64 - (5 * E3) / 256;
|
||||||
|
var M2 = (3 * E) / 8 + (3 * E2) / 32 + (45 * E3) / 1024;
|
||||||
|
var M3 = (15 * E2) / 256 + (45 * E3) / 1024;
|
||||||
|
var M4 = (35 * E3) / 3072;
|
||||||
|
|
||||||
|
var P2 = (3 / 2) * _E - (27 / 32) * _E3 + (269 / 512) * _E5;
|
||||||
|
var P3 = (21 / 16) * _E2 - (55 / 32) * _E4;
|
||||||
|
var P4 = (151 / 96) * _E3 - (417 / 128) * _E5;
|
||||||
|
var P5 = (1097 / 512) * _E4;
|
||||||
|
|
||||||
|
var R = 6378137;
|
||||||
|
|
||||||
|
var ZONE_LETTERS = "CDEFGHJKLMNPQRSTUVWXX";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTM坐标转WGS84坐标
|
||||||
|
* @param easting
|
||||||
|
* @param northing
|
||||||
|
* @param zoneNum utm带号
|
||||||
|
* @param zoneLetter
|
||||||
|
* @param northern
|
||||||
|
* @param strict
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function toLatLon(
|
||||||
|
easting: number,
|
||||||
|
northing: number,
|
||||||
|
zoneNum: number,
|
||||||
|
zoneLetter: string | null = null,
|
||||||
|
northern: boolean = true,
|
||||||
|
strict?: boolean
|
||||||
|
) {
|
||||||
|
strict = strict !== undefined ? strict : true;
|
||||||
|
|
||||||
|
if (!zoneLetter && northern === undefined) {
|
||||||
|
throw new Error("either zoneLetter or northern needs to be set");
|
||||||
|
} else if (zoneLetter && northern !== undefined) {
|
||||||
|
throw new Error("set either zoneLetter or northern, but not both");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strict) {
|
||||||
|
if (easting < 100000 || 1000000 <= easting) {
|
||||||
|
throw new RangeError(
|
||||||
|
"easting out of range (must be between 100 000 m and 999 999 m)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (northing < 0 || northing > 10000000) {
|
||||||
|
throw new RangeError(
|
||||||
|
"northing out of range (must be between 0 m and 10 000 000 m)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (zoneNum < 1 || zoneNum > 60) {
|
||||||
|
throw new RangeError("zone number out of range (must be between 1 and 60)");
|
||||||
|
}
|
||||||
|
if (zoneLetter) {
|
||||||
|
zoneLetter = zoneLetter.toUpperCase();
|
||||||
|
if (zoneLetter.length !== 1 || ZONE_LETTERS.indexOf(zoneLetter) === -1) {
|
||||||
|
throw new RangeError(
|
||||||
|
"zone letter out of range (must be between C and X)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
northern = zoneLetter >= "N";
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = easting - 500000;
|
||||||
|
var y = northing;
|
||||||
|
|
||||||
|
if (!northern) y -= 1e7;
|
||||||
|
|
||||||
|
var m = y / K0;
|
||||||
|
var mu = m / (R * M1);
|
||||||
|
|
||||||
|
var pRad =
|
||||||
|
mu +
|
||||||
|
P2 * Math.sin(2 * mu) +
|
||||||
|
P3 * Math.sin(4 * mu) +
|
||||||
|
P4 * Math.sin(6 * mu) +
|
||||||
|
P5 * Math.sin(8 * mu);
|
||||||
|
|
||||||
|
var pSin = Math.sin(pRad);
|
||||||
|
var pSin2 = Math.pow(pSin, 2);
|
||||||
|
|
||||||
|
var pCos = Math.cos(pRad);
|
||||||
|
|
||||||
|
var pTan = Math.tan(pRad);
|
||||||
|
var pTan2 = Math.pow(pTan, 2);
|
||||||
|
var pTan4 = Math.pow(pTan, 4);
|
||||||
|
|
||||||
|
var epSin = 1 - E * pSin2;
|
||||||
|
var epSinSqrt = Math.sqrt(epSin);
|
||||||
|
|
||||||
|
var n = R / epSinSqrt;
|
||||||
|
var r = (1 - E) / epSin;
|
||||||
|
|
||||||
|
var c = _E * pCos * pCos;
|
||||||
|
var c2 = c * c;
|
||||||
|
|
||||||
|
var d = x / (n * K0);
|
||||||
|
var d2 = Math.pow(d, 2);
|
||||||
|
var d3 = Math.pow(d, 3);
|
||||||
|
var d4 = Math.pow(d, 4);
|
||||||
|
var d5 = Math.pow(d, 5);
|
||||||
|
var d6 = Math.pow(d, 6);
|
||||||
|
|
||||||
|
var latitude =
|
||||||
|
pRad -
|
||||||
|
(pTan / r) *
|
||||||
|
(d2 / 2 - (d4 / 24) * (5 + 3 * pTan2 + 10 * c - 4 * c2 - 9 * E_P2)) +
|
||||||
|
(d6 / 720) * (61 + 90 * pTan2 + 298 * c + 45 * pTan4 - 252 * E_P2 - 3 * c2);
|
||||||
|
var longitude =
|
||||||
|
(d -
|
||||||
|
(d3 / 6) * (1 + 2 * pTan2 + c) +
|
||||||
|
(d5 / 120) * (5 - 2 * c + 28 * pTan2 - 3 * c2 + 8 * E_P2 + 24 * pTan4)) /
|
||||||
|
pCos;
|
||||||
|
|
||||||
|
return {
|
||||||
|
latitude: toDegrees(latitude),
|
||||||
|
longitude: toDegrees(longitude) + zoneNumberToCentralLongitude(zoneNum),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WGS84坐标转UTM坐标
|
||||||
|
* @param latitude
|
||||||
|
* @param longitude
|
||||||
|
* @param forceZoneNum
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function fromLatLon(
|
||||||
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
forceZoneNum?: number
|
||||||
|
) {
|
||||||
|
if (latitude > 84 || latitude < -80) {
|
||||||
|
throw new RangeError(
|
||||||
|
"latitude out of range (must be between 80 deg S and 84 deg N)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (longitude > 180 || longitude < -180) {
|
||||||
|
throw new RangeError(
|
||||||
|
"longitude out of range (must be between 180 deg W and 180 deg E)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var latRad = toRadians(latitude);
|
||||||
|
var latSin = Math.sin(latRad);
|
||||||
|
var latCos = Math.cos(latRad);
|
||||||
|
|
||||||
|
var latTan = Math.tan(latRad);
|
||||||
|
var latTan2 = Math.pow(latTan, 2);
|
||||||
|
var latTan4 = Math.pow(latTan, 4);
|
||||||
|
|
||||||
|
var zoneNum: number;
|
||||||
|
|
||||||
|
if (forceZoneNum === undefined) {
|
||||||
|
zoneNum = latLonToZoneNumber(latitude, longitude);
|
||||||
|
} else {
|
||||||
|
zoneNum = forceZoneNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
var zoneLetter = latitudeToZoneLetter(latitude);
|
||||||
|
|
||||||
|
var lonRad = toRadians(longitude);
|
||||||
|
var centralLon = zoneNumberToCentralLongitude(zoneNum);
|
||||||
|
var centralLonRad = toRadians(centralLon);
|
||||||
|
|
||||||
|
var n = R / Math.sqrt(1 - E * latSin * latSin);
|
||||||
|
var c = E_P2 * latCos * latCos;
|
||||||
|
|
||||||
|
var a = latCos * (lonRad - centralLonRad);
|
||||||
|
var a2 = Math.pow(a, 2);
|
||||||
|
var a3 = Math.pow(a, 3);
|
||||||
|
var a4 = Math.pow(a, 4);
|
||||||
|
var a5 = Math.pow(a, 5);
|
||||||
|
var a6 = Math.pow(a, 6);
|
||||||
|
|
||||||
|
var m =
|
||||||
|
R *
|
||||||
|
(M1 * latRad -
|
||||||
|
M2 * Math.sin(2 * latRad) +
|
||||||
|
M3 * Math.sin(4 * latRad) -
|
||||||
|
M4 * Math.sin(6 * latRad));
|
||||||
|
var easting =
|
||||||
|
K0 *
|
||||||
|
n *
|
||||||
|
(a +
|
||||||
|
(a3 / 6) * (1 - latTan2 + c) +
|
||||||
|
(a5 / 120) * (5 - 18 * latTan2 + latTan4 + 72 * c - 58 * E_P2)) +
|
||||||
|
500000;
|
||||||
|
var northing =
|
||||||
|
K0 *
|
||||||
|
(m +
|
||||||
|
n *
|
||||||
|
latTan *
|
||||||
|
(a2 / 2 +
|
||||||
|
(a4 / 24) * (5 - latTan2 + 9 * c + 4 * c * c) +
|
||||||
|
(a6 / 720) * (61 - 58 * latTan2 + latTan4 + 600 * c - 330 * E_P2)));
|
||||||
|
if (latitude < 0) northing += 1e7;
|
||||||
|
|
||||||
|
return {
|
||||||
|
easting: easting,
|
||||||
|
northing: northing,
|
||||||
|
zoneNum: zoneNum,
|
||||||
|
zoneLetter: zoneLetter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function latitudeToZoneLetter(latitude: number) {
|
||||||
|
if (-80 <= latitude && latitude <= 84) {
|
||||||
|
return ZONE_LETTERS[Math.floor((latitude + 80) / 8)];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function latLonToZoneNumber(latitude: number, longitude: number) {
|
||||||
|
if (56 <= latitude && latitude < 64 && 3 <= longitude && longitude < 12)
|
||||||
|
return 32;
|
||||||
|
|
||||||
|
if (72 <= latitude && latitude <= 84 && longitude >= 0) {
|
||||||
|
if (longitude < 9) return 31;
|
||||||
|
if (longitude < 21) return 33;
|
||||||
|
if (longitude < 33) return 35;
|
||||||
|
if (longitude < 42) return 37;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor((longitude + 180) / 6) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoneNumberToCentralLongitude(zoneNum: number) {
|
||||||
|
return (zoneNum - 1) * 6 - 180 + 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDegrees(rad: number) {
|
||||||
|
return (rad / Math.PI) * 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRadians(deg: number) {
|
||||||
|
return (deg * Math.PI) / 180;
|
||||||
|
}
|
||||||
36
react-native/app.json
Normal file
36
react-native/app.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "beerbuddy",
|
||||||
|
"slug": "beerbuddy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#000000"
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": ["**/*"],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#000000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"expo-location",
|
||||||
|
{
|
||||||
|
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
react-native/assets/adaptive-icon.png
Normal file
BIN
react-native/assets/adaptive-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
react-native/assets/favicon.png
Normal file
BIN
react-native/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
react-native/assets/icon.png
Normal file
BIN
react-native/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
71
react-native/assets/index.html
Normal file
71
react-native/assets/index.html
Normal file
File diff suppressed because one or more lines are too long
BIN
react-native/assets/splash.png
Normal file
BIN
react-native/assets/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
6
react-native/babel.config.js
Normal file
6
react-native/babel.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = function(api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: ['babel-preset-expo'],
|
||||||
|
};
|
||||||
|
};
|
||||||
104
react-native/components/Map.tsx
Normal file
104
react-native/components/Map.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { useAssets } from "expo-asset";
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { useWindowDimensions, Alert, Button } from "react-native";
|
||||||
|
import WebView, { WebViewMessageEvent } from "react-native-webview";
|
||||||
|
import * as Location from "expo-location";
|
||||||
|
import { fromLatLon, toLatLon } from "../UTM_converter";
|
||||||
|
|
||||||
|
const Map = () => {
|
||||||
|
const [assets] = useAssets([require("../assets/index.html")]);
|
||||||
|
const [htmlString, setHtmlString] = useState<string>();
|
||||||
|
|
||||||
|
const dimensions = useWindowDimensions();
|
||||||
|
const webViewRef = useRef<WebView | null>();
|
||||||
|
|
||||||
|
// gets location
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||||
|
if (status !== "granted") return;
|
||||||
|
const location = await Location.getCurrentPositionAsync({
|
||||||
|
distanceInterval: 0, // for IOS
|
||||||
|
accuracy: Location.Accuracy.High,
|
||||||
|
timeInterval: 3000, // for Android
|
||||||
|
});
|
||||||
|
// makes injectable javascript string
|
||||||
|
const str = `window.passLocation(${location.coords.longitude}, ${location.coords.latitude}); true()`;
|
||||||
|
// passes string to webview - there it is handled by OpenLayers to change the current location
|
||||||
|
webViewRef.current?.injectJavaScript(str);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// loads assets
|
||||||
|
useEffect(() => {
|
||||||
|
if (assets) {
|
||||||
|
fetch(assets[0].localUri || "")
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((html) => setHtmlString(html));
|
||||||
|
}
|
||||||
|
}, [assets]);
|
||||||
|
|
||||||
|
// exits if no map passed in
|
||||||
|
if (!htmlString) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageHandler = (event: WebViewMessageEvent) => {
|
||||||
|
if (typeof event.nativeEvent.data == "string") {
|
||||||
|
const message = event.nativeEvent.data;
|
||||||
|
if (message === "new pin start") {
|
||||||
|
Alert.alert(
|
||||||
|
"New Pin",
|
||||||
|
`Please select location for new pin then press "OK"`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: "OK",
|
||||||
|
onPress: () => {
|
||||||
|
// makes injectable javascript string
|
||||||
|
const str = `window.placePin(); true()`;
|
||||||
|
// passes string to webview - there it is handled by OpenLayers to change the current location
|
||||||
|
webViewRef.current?.injectJavaScript(str);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else if (message.startsWith(`@`)) {
|
||||||
|
const coords = message.slice(1).split(",");
|
||||||
|
const lon = Number(coords[0]);
|
||||||
|
const lat = Number(coords[1]);
|
||||||
|
const UTM = fromLatLon(lat, lon);
|
||||||
|
Alert.alert(
|
||||||
|
"Coords",
|
||||||
|
`${UTM.easting}, ${UTM.northing}, ${UTM.zoneNum}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebView
|
||||||
|
ref={(currentRef) => (webViewRef.current = currentRef)}
|
||||||
|
injectedJavascript=""
|
||||||
|
source={{
|
||||||
|
html: htmlString,
|
||||||
|
}}
|
||||||
|
javaScriptEnabled
|
||||||
|
style={{
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height,
|
||||||
|
}}
|
||||||
|
scrollEnabled={false}
|
||||||
|
overScrollMode="never"
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
scalesPageToFit={false}
|
||||||
|
containerStyle={{
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
onMessage={messageHandler}
|
||||||
|
webviewDebuggingEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Map;
|
||||||
35
react-native/index.html
Normal file
35
react-native/index.html
Normal file
File diff suppressed because one or more lines are too long
22666
react-native/package-lock.json
generated
Normal file
22666
react-native/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
react-native/package.json
Normal file
37
react-native/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "beerbuddy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "node_modules/expo/AppEntry.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/webpack-config": "^19.0.0",
|
||||||
|
"expo": "~49.0.15",
|
||||||
|
"expo-asset": "^8.10.1",
|
||||||
|
"expo-constants": "^14.4.2",
|
||||||
|
"expo-linking": "^6.0.0",
|
||||||
|
"expo-location": "~16.1.0",
|
||||||
|
"expo-router": "^2.0.14",
|
||||||
|
"expo-status-bar": "~1.6.0",
|
||||||
|
"install": "^0.13.0",
|
||||||
|
"npx": "^10.2.2",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-native": "0.72.6",
|
||||||
|
"react-native-gesture-handler": "^2.14.0",
|
||||||
|
"react-native-safe-area-context": "^4.7.4",
|
||||||
|
"react-native-screens": "^3.27.0",
|
||||||
|
"react-native-web": "~0.19.6",
|
||||||
|
"react-native-webview": "^13.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.20.0",
|
||||||
|
"@types/react": "~18.2.14",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
6
react-native/tsconfig.json
Normal file
6
react-native/tsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": "expo/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
7
react-native/webpack.config.js
Normal file
7
react-native/webpack.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
|
||||||
|
|
||||||
|
module.exports = async function (env, argv) {
|
||||||
|
const config = await createExpoWebpackConfigAsync(env, argv);
|
||||||
|
// Customize the config before returning it.
|
||||||
|
return config;
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue