Hello world
This commit is contained in:
commit
4b42391f4e
|
@ -0,0 +1 @@
|
|||
notes.txt
|
|
@ -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>
|
|
@ -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}`;
|
||||
}
|
|
@ -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 };
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue