Hello world

This commit is contained in:
Basti 2020-01-02 13:51:50 -05:00
commit 4b42391f4e
No known key found for this signature in database
GPG Key ID: BE4634D632D39B67
13 changed files with 539 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
notes.txt

107
index.html Normal file
View 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
View 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
View 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
View 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
View 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;

View 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
View 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
View 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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

1
vendor/widget.js.map vendored Normal file

File diff suppressed because one or more lines are too long