Hello world
This commit is contained in:
commit
4b42391f4e
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
notes.txt
|
107
index.html
Normal file
107
index.html
Normal file
|
@ -0,0 +1,107 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>RS Location</title>
|
||||
<script type="text/javascript" src="vendor/remotestorage.js"></script>
|
||||
<script type="text/javascript" src="vendor/widget.js"></script>
|
||||
<script type="module" src="main.mjs"></script>
|
||||
<link rel="stylesheet" href="style.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>RS Location</h1>
|
||||
<p>
|
||||
This page lets you geocode your current location (centered on the nearest
|
||||
city, or entered manually), and upload both a GeoJSON document as well as
|
||||
map image to your remote storage.
|
||||
</p>
|
||||
|
||||
<div class="view disconnected hidden">
|
||||
<div id="rs-widget-container"></div>
|
||||
</div>
|
||||
|
||||
<main class="view connected hidden">
|
||||
<section class="settings">
|
||||
<h2>Settings</h2>
|
||||
<p class="rs-account">
|
||||
Storage connected:
|
||||
<strong><span class="user-address"></span></strong>
|
||||
<button class="disconnect">Disconnect</button>
|
||||
</p>
|
||||
<h3>Geocoding</h3>
|
||||
<p>
|
||||
<label>
|
||||
OpenCage API key/token:<br>
|
||||
<input type="text" class="api-key opencage">
|
||||
</label>
|
||||
</p>
|
||||
<h3>Maps</h3>
|
||||
<p>
|
||||
<label>
|
||||
Mapbox API key/token:<br>
|
||||
<input type="text" class="api-key mapbox">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
Map zoom factor:
|
||||
<select class="map-zoom-factor">
|
||||
<option value="0" selected="selected">0</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<section class="location-actions">
|
||||
<h2>New location</h2>
|
||||
<p>
|
||||
<button class="get-coordinates">Get current/GPS location</button>
|
||||
</p>
|
||||
<form class="get-location">
|
||||
<p>
|
||||
<input class="location" placeholder="City, Country">
|
||||
<input type="submit" value="Get location">
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="current-location hidden">
|
||||
<h3>Current/GPS location</h3>
|
||||
<p class="coords"></p>
|
||||
<p class="formatted-result"></p>
|
||||
</section>
|
||||
<section class="current-city hidden">
|
||||
<h3>Current city</h3>
|
||||
<p class="coords"></p>
|
||||
<p class="formatted-result"></p>
|
||||
<p class="map"></p>
|
||||
<p>
|
||||
<button class="publish">Publish</button>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<section class="profile public hidden">
|
||||
<h2>Public location profile</h2>
|
||||
<p class="link"></p>
|
||||
<p class="coords"></p>
|
||||
<p class="formatted"></p>
|
||||
<p class="map"></p>
|
||||
</section>
|
||||
<section class="profile private hidden">
|
||||
<h2>Private location profile</h2>
|
||||
<p class="coords"></p>
|
||||
<p class="formatted"></p>
|
||||
<p class="map"></p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
238
main.mjs
Normal file
238
main.mjs
Normal file
|
@ -0,0 +1,238 @@
|
|||
import ApiKeys from './modules/rs-module-api-keys.mjs';
|
||||
import Geocoder from './modules/geocode.mjs';
|
||||
import initializeSettings from './modules/settings.mjs';
|
||||
import initializeProfileUpdates from './modules/profile.mjs';
|
||||
import { showElement, hideElement, renderImage } from './modules/dom-helpers.mjs';
|
||||
|
||||
const remoteStorage = new RemoteStorage({ modules: [ApiKeys] });
|
||||
remoteStorage.access.claim('profile', 'rw');
|
||||
remoteStorage.access.claim('api-keys', 'rw');
|
||||
remoteStorage.caching.enable('/profile/');
|
||||
remoteStorage.caching.enable('/public/profile/');
|
||||
|
||||
const widget = new Widget(remoteStorage, {
|
||||
modalBackdrop: true,
|
||||
skipInitial: true
|
||||
});
|
||||
widget.attach('rs-widget-container');
|
||||
|
||||
document.querySelector('.rs-account .disconnect').addEventListener('click', e => {
|
||||
remoteStorage.disconnect();
|
||||
});
|
||||
|
||||
let data = {
|
||||
currentUserLocation: {
|
||||
coords: null,
|
||||
openCageResult: null
|
||||
},
|
||||
currentCity: {
|
||||
coords: null,
|
||||
openCageResult: null,
|
||||
geoJSON: null,
|
||||
imageBlob: null,
|
||||
imageObjectURL: null
|
||||
}
|
||||
};
|
||||
|
||||
data.currentUserLocation = new Proxy(data.currentUserLocation, {
|
||||
set (obj, prop, value) {
|
||||
obj[prop] = value;
|
||||
|
||||
switch(prop) {
|
||||
case 'coords':
|
||||
renderCoordinates();
|
||||
break;
|
||||
case 'openCageResult':
|
||||
renderFormattedResult();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
data.currentCity = new Proxy(data.currentCity, {
|
||||
set (obj, prop, value) {
|
||||
obj[prop] = value;
|
||||
|
||||
switch(prop) {
|
||||
case 'coords':
|
||||
fetchMapImage();
|
||||
break;
|
||||
case 'openCageResult':
|
||||
renderFormattedCityResult();
|
||||
createGeoJSON();
|
||||
break;
|
||||
case 'geoJSON':
|
||||
break;
|
||||
case 'imageBlob':
|
||||
break;
|
||||
case 'imageObjectURL':
|
||||
renderMapImage();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
remoteStorage.on('not-connected', () => {
|
||||
showElement('.view.disconnected');
|
||||
});
|
||||
|
||||
remoteStorage.on('connected', () => {
|
||||
hideElement('.view.disconnected');
|
||||
showElement('.view.connected');
|
||||
|
||||
document.querySelector('.rs-account .user-address').innerHTML = remoteStorage.remote.userAddress;
|
||||
|
||||
initializeSettings(remoteStorage);
|
||||
initializeProfileUpdates(remoteStorage, data);
|
||||
});
|
||||
|
||||
remoteStorage.on('disconnected', () => {
|
||||
hideElement('.view.connected');
|
||||
showElement('.view.disconnected');
|
||||
});
|
||||
|
||||
const geocoder = new Geocoder(remoteStorage);
|
||||
|
||||
const mapBoxStyle = 'v1/mapbox/streets-v10'
|
||||
const imageSize = '260x100@2x'
|
||||
|
||||
document.querySelector('button.get-coordinates').addEventListener('click', () => {
|
||||
console.debug('Getting current/GPS location...');
|
||||
navigator.geolocation.getCurrentPosition(async position => {
|
||||
console.debug('Location data:', position);
|
||||
data.currentUserLocation.coords = {
|
||||
lat: position.coords.latitude.toFixed(4),
|
||||
lng: position.coords.longitude.toFixed(4)
|
||||
};
|
||||
await geocodeUserLocation();
|
||||
geocodeNearestCity();
|
||||
});
|
||||
})
|
||||
|
||||
document.querySelector('form.get-location').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
console.debug('Geocoding supplied location...');
|
||||
geocodeLocationInput().catch(err => window.alert(err));
|
||||
})
|
||||
|
||||
async function geocodeUserLocation () {
|
||||
console.debug('Geocoding coordinates...');
|
||||
const { lat, lng } = data.currentUserLocation.coords;
|
||||
|
||||
return geocoder.reverse(lat, lng).then(res => {
|
||||
const result = res.results[0];
|
||||
console.debug('Result:', result);
|
||||
data.currentUserLocation.openCageResult = result;
|
||||
})
|
||||
}
|
||||
|
||||
function geocodeNearestCity () {
|
||||
console.debug('Geocoding nearest city...');
|
||||
const result = data.currentUserLocation.openCageResult;
|
||||
let { city, town, village, hamlet, county } = result.components;
|
||||
if (!city) { city = town || village || hamlet || county; }
|
||||
const query = `${city}, ${result.components.state}, ${result.components['ISO_3166-1_alpha-2']}&no_record=1&min_confidence=3`;
|
||||
|
||||
return geocoder.geocode(query).then(res => {
|
||||
const result = res.results[0];
|
||||
console.debug('Result:', result);
|
||||
data.currentCity.coords = {
|
||||
lat: result.geometry.lat.toFixed(4),
|
||||
lng: result.geometry.lng.toFixed(4)
|
||||
};
|
||||
data.currentCity.openCageResult = result;
|
||||
})
|
||||
}
|
||||
|
||||
function geocodeLocationInput () {
|
||||
console.debug('Geocoding supplied location...');
|
||||
const input = document.querySelector('input.location').value;
|
||||
const query = `${input},&no_record=1&min_confidence=3`;
|
||||
|
||||
return geocoder.geocode(query).then(res => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const result = res.results[0];
|
||||
console.debug('Result:', result);
|
||||
if (result) {
|
||||
data.currentCity.coords = {
|
||||
lat: result.geometry.lat.toFixed(4),
|
||||
lng: result.geometry.lng.toFixed(4)
|
||||
};
|
||||
data.currentCity.openCageResult = result;
|
||||
resolve();
|
||||
} else {
|
||||
reject('Nothing found');
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function createGeoJSON () {
|
||||
const res = data.currentCity.openCageResult;
|
||||
const { city, town, village, hamlet, county } = res.components;
|
||||
data.currentCity.geoJSON = {
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [res.geometry.lng, res.geometry.lat]
|
||||
},
|
||||
"city": city || town || village || hamlet,
|
||||
"state": res.components.state,
|
||||
"country": res.components.country,
|
||||
"timezone": res.annotations.timezone
|
||||
}
|
||||
console.debug('Created GeoJSON object:', data.currentCity.geojson);
|
||||
}
|
||||
|
||||
async function fetchMapImage () {
|
||||
const mapBoxToken = await remoteStorage.apiKeys.get('mapbox').then(c => c.token);
|
||||
const zoomFactor = localStorage.getItem('rs-location:map-zoom-factor');
|
||||
const { lat, lng } = data.currentCity.coords;
|
||||
const url = encodeURI(`https://api.mapbox.com/styles/${mapBoxStyle}/static/pin-s-harbor+fff(${lng},${lat})/${lng},${lat},${zoomFactor}/${imageSize}?access_token=${mapBoxToken}`)
|
||||
console.debug('Fetching map image...');
|
||||
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(imageData => {
|
||||
const url = URL.createObjectURL(imageData);
|
||||
data.currentCity.imageBlob = imageData;
|
||||
data.currentCity.imageObjectURL = url
|
||||
|
||||
return fetch(url)
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(arrayBuffer => {
|
||||
data.currentCity.imageArrayBuffer = arrayBuffer;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function renderMapImage () {
|
||||
const url = data.currentCity.imageObjectURL;
|
||||
renderImage('section.current-city .map', url, '260x100');
|
||||
}
|
||||
|
||||
function renderCoordinates () {
|
||||
showElement('section.current-location');
|
||||
const coordsEl = document.querySelector('section.current-location .coords');
|
||||
const coords = data.currentUserLocation.coords;
|
||||
coordsEl.innerHTML = `Latitude: ${coords.lat}<br>Longitude: ${coords.lng}`;
|
||||
}
|
||||
|
||||
function renderFormattedResult () {
|
||||
const el = document.querySelector('section.current-location .formatted-result');
|
||||
el.innerHTML = data.currentUserLocation.openCageResult.formatted;
|
||||
}
|
||||
|
||||
function renderFormattedCityResult () {
|
||||
showElement('section.current-city');
|
||||
const coordsEl = document.querySelector('section.current-city .coords');
|
||||
const coords = data.currentCity.coords;
|
||||
coordsEl.innerHTML = `Latitude: ${coords.lat}<br>Longitude: ${coords.lng}`;
|
||||
|
||||
const result = data.currentCity.openCageResult;
|
||||
const el = document.querySelector('section.current-city .formatted-result');
|
||||
const { city, town, village, hamlet, county } = result.components;
|
||||
el.innerHTML = `${city || town || village || hamlet || county}, ${result.components.country}`;
|
||||
}
|
28
modules/dom-helpers.mjs
Normal file
28
modules/dom-helpers.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
function hideElement (selector) {
|
||||
document.querySelector(selector)
|
||||
.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showElement (selector) {
|
||||
document.querySelector(selector)
|
||||
.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function updateHTML (selector, content) {
|
||||
document.querySelector(selector).innerHTML = content;
|
||||
}
|
||||
|
||||
function renderImage (parentSelector, imageUrl, size) {
|
||||
const imageEl = document.createElement('img');
|
||||
imageEl.setAttribute("src", imageUrl);
|
||||
if (size) {
|
||||
const [ width, height ] = size.split('x');
|
||||
imageEl.setAttribute("width", width);
|
||||
imageEl.setAttribute("height", height);
|
||||
}
|
||||
const parentEl = document.querySelector(parentSelector);
|
||||
parentEl.innerHTML = '';
|
||||
parentEl.appendChild(imageEl);
|
||||
}
|
||||
|
||||
export { hideElement, showElement, updateHTML, renderImage };
|
20
modules/geocode.mjs
Normal file
20
modules/geocode.mjs
Normal file
|
@ -0,0 +1,20 @@
|
|||
export default class Geocoder {
|
||||
|
||||
constructor (remoteStorage) {
|
||||
this.rs = remoteStorage;
|
||||
}
|
||||
|
||||
async reverse (lat, lng) {
|
||||
const q = `${lat}+${lng}&no_record=1&min_confidence=3`;
|
||||
return this.geocode(q);
|
||||
}
|
||||
|
||||
async geocode (q) {
|
||||
const openCageKey = await this.rs.apiKeys.get('opencage').then(c => c.token);
|
||||
const response = await fetch(
|
||||
`https://api.opencagedata.com/geocode/v1/json?key=${openCageKey}&q=${q}&no_record=1`
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
}
|
46
modules/profile.mjs
Normal file
46
modules/profile.mjs
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { showElement, updateHTML, renderImage } from './dom-helpers.mjs';
|
||||
|
||||
function formattedCoordinates (geo) {
|
||||
return `Latitude: ${geo.geometry.coordinates[1]}<br>` +
|
||||
`Longitude: ${geo.geometry.coordinates[0]}<br>`;
|
||||
}
|
||||
|
||||
function formattedLocation (geo) {
|
||||
return `${geo.city || geo.state}, ${geo.country}`;
|
||||
}
|
||||
|
||||
async function renderPublicProfile (geo) {
|
||||
showElement('.profile.public');
|
||||
updateHTML('.profile.public .coords', formattedCoordinates(geo));
|
||||
updateHTML('.profile.public .formatted', formattedLocation(geo));
|
||||
}
|
||||
|
||||
async function initializeProfileUpdates(remoteStorage, data) {
|
||||
const privateClient = remoteStorage.scope('/profile/');
|
||||
const publicClient = remoteStorage.scope('/public/profile/');
|
||||
const profileUrl = await publicClient.getItemURL('current-location');
|
||||
const imageUrl = await publicClient.getItemURL('current-location.png');
|
||||
updateHTML('.profile.public .link', `<a href="${profileUrl}">Public URL</a>`);
|
||||
|
||||
await publicClient.getObject('current-location').then(async res => {
|
||||
if (res) {
|
||||
renderPublicProfile(res);
|
||||
renderImage('.profile.public .map', imageUrl, '260x100');
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelector('.current-city button.publish').addEventListener('click', async () => {
|
||||
const content = JSON.stringify(data.currentCity.geoJSON);
|
||||
const mapImageData = data.currentCity.imageArrayBuffer;
|
||||
|
||||
publicClient.storeFile('application/geo+json', 'current-location', content)
|
||||
.then(renderPublicProfile(data.currentCity.geoJSON));
|
||||
|
||||
publicClient.storeFile('image/png', 'current-location.png', mapImageData)
|
||||
.then(() => {
|
||||
renderImage('.profile.public .map', `${imageUrl}?${new Date().getTime()}`, '260x100');
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export default initializeProfileUpdates;
|
23
modules/rs-module-api-keys.mjs
Normal file
23
modules/rs-module-api-keys.mjs
Normal file
|
@ -0,0 +1,23 @@
|
|||
const ApiKeys = { name: 'api-keys', builder: function (privateClient, publicClient) {
|
||||
privateClient.declareType('credentials', {
|
||||
// TODO add schema
|
||||
});
|
||||
|
||||
return {
|
||||
exports: {
|
||||
set (serviceName, credentials = {}) {
|
||||
return privateClient.storeObject('credentials', serviceName, credentials);
|
||||
},
|
||||
|
||||
get (serviceName) {
|
||||
return privateClient.getObject(serviceName);
|
||||
},
|
||||
|
||||
remove (serviceName) {
|
||||
return privateClient.remove(serviceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
export default ApiKeys;
|
32
modules/settings.mjs
Normal file
32
modules/settings.mjs
Normal file
|
@ -0,0 +1,32 @@
|
|||
async function initializeSettings(remoteStorage) {
|
||||
//
|
||||
// API Keys (remoteStorage)
|
||||
//
|
||||
['opencage', 'mapbox'].forEach(async service => {
|
||||
let inputEl = document.querySelector(`input.api-key.${service}`);
|
||||
|
||||
await remoteStorage.apiKeys.get(service).then(credentials => {
|
||||
if (credentials) inputEl.value = credentials.token;
|
||||
})
|
||||
|
||||
inputEl.addEventListener('change', e => {
|
||||
if (e.target.value.length > 0) {
|
||||
remoteStorage.apiKeys.set(service, { token: e.target.value });
|
||||
} else {
|
||||
remoteStorage.apiKeys.remove(service);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
//
|
||||
// Map settings (localStorage)
|
||||
//
|
||||
const zoomFactorSelectEl = document.querySelector('.settings .map-zoom-factor');
|
||||
const zoomFactor = localStorage.getItem('rs-location:map-zoom-factor');
|
||||
if (zoomFactor) zoomFactorSelectEl.value = zoomFactor;
|
||||
zoomFactorSelectEl.addEventListener('change', e => {
|
||||
localStorage.setItem('rs-location:map-zoom-factor', e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
export default initializeSettings;
|
13
style.css
Normal file
13
style.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
font-family: system-ui;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-column-gap: 2rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
27
vendor/remotestorage.js
vendored
Normal file
27
vendor/remotestorage.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/remotestorage.js.map
vendored
Normal file
1
vendor/remotestorage.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
vendor/widget.js
vendored
Normal file
2
vendor/widget.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/widget.js.map
vendored
Normal file
1
vendor/widget.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user