This commit is contained in:
Win 2023-04-08 10:34:31 +07:00
parent 27866ba7fc
commit b57d20d055
21 changed files with 537 additions and 49 deletions

View File

@ -1,5 +1,6 @@
{ {
"domain": "localhost", "domain": "",
"host": "localhost",
"port": 7000, "port": 7000,
"openWebBrowser": true, "openWebBrowser": true,
"clientId": "XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com", "clientId": "XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com",

View File

@ -1,6 +1,7 @@
const config = require('./config.json') const config = require('./config.json')
const domain = config.domain; const domain = config.domain;
const host = config.host;
const port = config.port; const port = config.port;
const openWebBrowser = config.openWebBrowser; // Set to false if running as a server const openWebBrowser = config.openWebBrowser; // Set to false if running as a server
@ -12,10 +13,11 @@ const clientSecret = config.clientSecret // Client Secret from Google API page
const open = require('open'); const open = require('open');
if (openWebBrowser) { if (openWebBrowser) {
(async () => { (async () => {
await open(`http://${domain}:${port}/`); await open(`http://${host}:${port}/`);
})(); })();
} }
const favicon = require('serve-favicon');
const express = require('express') const express = require('express')
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser') const cookieParser = require('cookie-parser')
@ -31,19 +33,42 @@ app.use(
limits: {fileSize: 75 * 1024 * 1024}, limits: {fileSize: 75 * 1024 * 1024},
}) })
); );
// CSS and JS Files
app.use(express.static(__dirname + '/public'));
app.set('view engine', 'ejs');
app.use(favicon(__dirname + '/public/assets/icons/favicon.ico'));
app.get('/', function (req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/views/index.html')
res.render('pages/index', {
domain: "map.winscloud.net",
clientId: clientId
});
}) })
app.get('/upload', function(req, res){ app.get('/upload', function(req, res){
res.sendFile(__dirname + '/views/upload.html') res.render('pages/upload');
}) })
app.post('/upload', function (req, res) { app.post('/upload', function (req, res) {
let latitude = req.body["lat"];
let longitude = req.body["long"];
let key = req.cookies["oauth"] let key = req.cookies["oauth"]
if (!key) { if (!key) {
return res.redirect('/') return res.redirect('/')
} else { } else {
if (!req.files) { if (!req.files) {
return res.status(400).send("Missing file!") return res.status(400).render('pages/error', {
errorCode: 400,
errorStatus: "Missing File",
errorMessage: "Missing File",
response: "Error: Missing File"
})
} else { } else {
// Part 1: Get uploadUrl // Part 1: Get uploadUrl
const options = { const options = {
@ -56,7 +81,13 @@ app.post('/upload', function (req, res) {
}; };
request(options, function (error, response) { request(options, function (error, response) {
if (error) { if (error) {
console.log(error) && res.status(500).send("Error with getting upload url"); console.log(error)
res.status(500).render('pages/error', {
errorCode: 500,
errorStatus: "ERROR",
errorMessage: "Error: Error with getting upload url",
response: JSON.stringify(JSON.parse(response.body), null, 4)
})
} else { } else {
let uploadUrl = JSON.parse(response.body)["uploadUrl"] let uploadUrl = JSON.parse(response.body)["uploadUrl"]
// PART 2: Upload the image! // PART 2: Upload the image!
@ -70,7 +101,15 @@ app.post('/upload', function (req, res) {
}; };
request(options, function (error) { request(options, function (error) {
if (error) { if (error) {
console.log(error) && res.status(500).send("Error with uploading file to uploadUrl"); console.log(error)
res.status(500).render('pages/error', {
errorCode: 500,
errorStatus: "UPLOAD ERROR",
errorMessage: "Error: Error with uploading file to Google's API",
response: error
})
} else { } else {
//PART 3: Set metadata! //PART 3: Set metadata!
let body; let body;
@ -81,8 +120,8 @@ app.post('/upload', function (req, res) {
}, },
"pose": { "pose": {
"latLngPair": { "latLngPair": {
"latitude": req.body["lat"], "latitude": latitude,
"longitude": req.body["long"] "longitude": longitude
}, },
"heading": 0 "heading": 0
} }
@ -106,12 +145,30 @@ app.post('/upload', function (req, res) {
}; };
request(options, function (error, response) { request(options, function (error, response) {
if (error) { if (error) {
console.log(error) && res.status(500).send("Error with setting metadata of file"); console.log(error)
res.status(500).render('pages/error', {
errorCode: 500,
errorStatus: "ERROR",
errorMessage: "Error with setting metadata of file",
response: "Error: Error with setting metadata of file"
})
} else { } else {
if (JSON.parse(response.body)["error"]) { if (JSON.parse(response.body)["error"]) {
res.status(JSON.parse(response.body)["error"]["code"]).send(`Status: ${JSON.parse(response.body)["error"]["status"]}<br>Error message: ${JSON.parse(response.body)["error"]["message"]}</a><br><a href="/upload">Upload another?</a>`) res.status(500).render('pages/error', {
errorCode: JSON.parse(response.body)["error"]["code"],
errorStatus: JSON.parse(response.body)["error"]["status"],
errorMessage: JSON.parse(response.body)["error"]["message"],
response: JSON.stringify(JSON.parse(response.body), null, 4),
});
} else { } else {
res.status(200).send(`Status: ${JSON.parse(response.body)["mapsPublishStatus"]}<br>Link: <a href="${JSON.parse(response.body)["shareLink"]}">${JSON.parse(response.body)["shareLink"]}</a><br>You may have to wait awhile after uploading for Google to process the image.<br><a href="/upload">Upload another?</a>`) let shareLink = JSON.parse(response.body)["shareLink"]
res.status(200).render('pages/success', {
status: JSON.parse(response.body)["mapsPublishStatus"],
shareLink: shareLink,
response: JSON.stringify(JSON.parse(response.body), null, 4)
});
} }
} }
}); });
@ -127,7 +184,7 @@ app.get('/auth', function (req, res) {
const request = require('request'); const request = require('request');
const options = { const options = {
'method': 'POST', 'method': 'POST',
'url': `https://www.googleapis.com/oauth2/v4/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=authorization_code&code=${req.query["code"]}&redirect_uri=http://${domain}:${port}/auth/&scope=https://www.googleapis.com/auth/streetviewpublish`, 'url': `https://www.googleapis.com/oauth2/v4/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=authorization_code&code=${req.query["code"]}&redirect_uri=https://${domain}/auth/&scope=https://www.googleapis.com/auth/streetviewpublish`,
'headers': {} 'headers': {}
}; };
request(options, function (error, response) { request(options, function (error, response) {
@ -136,7 +193,7 @@ app.get('/auth', function (req, res) {
maxAge: JSON.parse(response.body)["expires_in"] * 1000, maxAge: JSON.parse(response.body)["expires_in"] * 1000,
httpOnly: true httpOnly: true
}); });
res.sendFile(__dirname + '/views/upload.html') res.render('pages/upload')
}); });
}) })

View File

@ -12,9 +12,11 @@
"dependencies": { "dependencies": {
"body-parser": "^1.20.1", "body-parser": "^1.20.1",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"ejs": "^3.1.9",
"express": "^4.18.2", "express": "^4.18.2",
"express-fileupload": "^1.4.0", "express-fileupload": "^1.4.0",
"open": "^8.4.0", "open": "^8.4.0",
"request": "^2.88.2" "request": "^2.88.2",
"serve-favicon": "^2.5.0"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -0,0 +1,34 @@
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('.image-upload-wrap').hide();
$('.file-upload-image').attr('src', e.target.result);
$('.file-upload-content').show();
$('.image-title').html(input.files[0].name);
};
reader.readAsDataURL(input.files[0]);
} else {
removeUpload();
}
}
function removeUpload() {
$('.file-upload-input').replaceWith($('.file-upload-input').clone());
$('.file-upload-content').hide();
$('.image-upload-wrap').show();
}
$('.image-upload-wrap').bind('dragover', function() {
$('.image-upload-wrap').addClass('image-dropping');
});
$('.image-upload-wrap').bind('dragleave', function() {
$('.image-upload-wrap').removeClass('image-dropping');
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

@ -0,0 +1,39 @@
// Creating map options
let mapOptions = {
center: [17.385044, 78.486671],
zoom: 10,
}
var defaultIcon = L.icon({
iconUrl: '/assets/icons/marker-icon.png',
shadowUrl: '/assets/icons/marker-shadow.png',
});
// Creating a map object
let map = new L.map('map', mapOptions);
// Creating a Layer object
let layer = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
// Geolocation
L.control.locate().addTo(map);
// Adding layer to the map
map.addLayer(layer);
// Marker
let marker = null;
map.on('click', (event)=> {
if(marker !== null){
map.removeLayer(marker);
}
marker = L.marker([event.latlng.lat , event.latlng.lng], {icon: defaultIcon}).addTo(map);
document.getElementById('lat').value = event.latlng.lat;
document.getElementById('long').value = event.latlng.lng;
})

File diff suppressed because one or more lines are too long

17
public/assets/material/material.min.js vendored Normal file

File diff suppressed because one or more lines are too long

139
public/assets/style.css Normal file
View File

@ -0,0 +1,139 @@
body {
margin: 0;
padding: 0;
}
button {
border: none;
border-radius: 2px;
padding: 12px 18px;
font-size: 16px;
text-transform: uppercase;
cursor: pointer;
color: white;
background-color: #2196f3;
box-shadow: 0 0 4px #999;
outline: none;
}
pre {
white-space: pre-line;
}
form, input, label, p {
color: white !important;
}
#icon {
max-width: 45px;
height: auto;
border-radius: 50%;
}
.column {
flex-grow: 1;
display: inline-block;
}
/* Ripple effect */
.ripple {
background-position: center;
transition: background 0.8s;
}
.ripple:hover {
background: #47a7f5 radial-gradient(circle, transparent 1%, #47a7f5 1%) center/15000%;
}
.ripple:active {
background-color: #6eb9f7;
background-size: 100%;
transition: background 0s;
}
/* DRAG AND DROP */
.file-upload {
width: 100%;
margin: 0 auto;
}
.file-upload-btn {
width: 100%;
margin: 0;
color: #fff;
background: #2196f3;
border: none;
padding: 10px;
border-radius: 4px;
border-bottom: 4px solid #2879f3;
transition: all .2s ease;
outline: none;
text-transform: uppercase;
font-weight: 700;
}
.file-upload-btn:hover {
background: #2175f3;
color: #ffffff;
transition: all .2s ease;
cursor: pointer;
}
.file-upload-btn:active {
border: 0;
transition: all .2s ease;
}
.file-upload-content {
display: none;
text-align: center;
}
.file-upload-input {
position: absolute;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
outline: none;
opacity: 0;
cursor: pointer;
}
.image-upload-wrap {
margin-top: 20px;
border: 4px dashed #2196f3;
position: relative;
}
.image-dropping, .image-upload-wrap:hover {
background-color: #2196f3;
border: 4px dashed #ffffff;
}
.image-title-wrap {
padding: 0 15px 15px 15px;
color: #222;
}
.drag-text {
text-align: center;
}
.drag-text .buttonText {
font-weight: 100;
text-transform: uppercase;
color: #fff;
padding: 60px 0;
}
.file-upload-image {
max-height: 200px;
max-width: 200px;
margin: auto;
padding: 20px;
}
.remove-image {
width: 200px;
margin: 0;
color: #fff;
background: #cd4535;
border: none;
padding: 10px;
border-radius: 4px;
border-bottom: 4px solid #b02818;
transition: all .2s ease;
outline: none;
text-transform: uppercase;
font-weight: 700;
}
.remove-image:hover {
background: #c13b2a;
color: #ffffff;
transition: all .2s ease;
cursor: pointer;
}
.remove-image:active {
border: 0;
transition: all .2s ease;
}

View File

@ -1,7 +0,0 @@
<div style="text-align: center;"><h1>PhotoSphereStudio</h1>
<p>I am not very good at CSS, so please submit a pull request if you are great at frontend development.</p>
<p>Use the button below to authenticate your google account in order to use the service.</p>
<a href="https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fstreetviewpublish&redirect_uri=http%3A%2F%2Flocalhost%3A7000%2Fauth%2F">
<img src="https://developers.google.com/static/identity/images/btn_google_signin_dark_normal_web.png">
</a>
</div>

36
views/pages/error.ejs Normal file
View File

@ -0,0 +1,36 @@
<%- include('../partials/header'); %>
<div class="column shadow-sm p-3 m-4 bg-dark-3 text-light">
<div align="left">
<div align="center">
<span class="material-symbols-outlined text-danger" style="font-size: 5vw;">
close
</span>
<p class="h1 text-danger">
<%= errorCode %>: <%= errorStatus %>
</p>
<p class="h3 text-danger">
<%= errorMessage %>
</p>
</div>
<div class="debugContainer">
<details>
<summary>Debug Information</summary>
<pre class="text-light"><code><%= response %></code></pre>
</details>
</div>
<div class="uploadButtonContainer">
<a href="/upload">
<button class="ripple">TRY AGAIN</button>
</a>
</div>
</div>
</div>
<%- include('../partials/footer'); %>

13
views/pages/index.ejs Normal file
View File

@ -0,0 +1,13 @@
<%- include('../partials/header'); %>
<div class="column shadow-sm p-3 m-4 bg-dark-3 text-light">
<div align="left">
<h1>Get Started</h1>
<p>Use the button below to authenticate your google account in order to use the service.</p>
<a href="https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=<%= clientId %>&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fstreetviewpublish&redirect_uri=https://<%= domain %>/auth/">
<img src="https://developers.google.com/static/identity/images/btn_google_signin_dark_normal_web.png">
</a>
</div>
</div>
<%- include('../partials/footer'); %>

44
views/pages/success.ejs Normal file
View File

@ -0,0 +1,44 @@
<%- include('../partials/header'); %>
<div class="column shadow-sm p-3 m-4 bg-dark-3 text-light">
<div align="left">
<div align="center">
<span class="material-symbols-outlined text-success" style="font-size: 5vw;">
done
</span>
<p class="h1 text-success">
<%= status %>
</p>
<p class="h3 text-success">
Your 360 image has been published to Google Street View! Click on the link below to view the image!
</p>
<div class="viewContainer">
<span>Link: </span><a href="<%= shareLink %>"><%= shareLink %></a>
</div>
<div class="alert alert-info" role="alert">
<span>You may have to wait awhile after uploading for Google to process the image.</span>
</div>
</div>
<div class="debugContainer">
<details>
<summary>Debug Information</summary>
<pre class="text-light"><code><%= response %></code></pre>
</details>
</div>
<div class="uploadButtonContainer">
<a href="/upload">
<button class="ripple">UPLOAD ANOTHER?</button>
</a>
</div>
</div>
</div>
<%- include('../partials/footer'); %>

70
views/pages/upload.ejs Normal file
View File

@ -0,0 +1,70 @@
<%- include('../partials/header'); %>
<div class="column shadow-sm p-3 m-4 bg-dark-3 text-light">
<div align="left">
<h1>Upload</h1>
<p>This page will allow you to upload photo spheres to Google Maps after the <i>idiots</i> at Google removed their perfectly working StreetView app.</p>
<div class="alert alert-info" role="alert">
<span>Images <b>must</b> have the proper 360 degree file properties, you will need to figure out how to add this metadata. <b>Google Pixel photosphere photos work perfectly</b>, I have yet to figure out how to get my DJI drone 360 photos working.</span>
</div>
<div class="alert alert-warning" role="alert">
<span>Latitude and Longitude is <b>not required</b> if your photo already contains location data, however if it doesn't, you can use the map below to pinpoint your location</span>
</div>
<form name="uploadForm" action="/upload" method="POST" enctype="multipart/form-data">
<div class="form-group">
<legend>Upload a 360 image</legend>
<div class="file-upload">
<button class="file-upload-btn" type="button" onclick="$('.file-upload-input').trigger('click')">Add Image</button>
<div class="image-upload-wrap">
<input class="file-upload-input" type='file' id="file" name="file" onchange="readURL(this);" accept="image/png, image/jpeg" />
<div class="drag-text">
<div class="buttonText">
<span class="material-symbols-outlined" style="font-size: 5vw;">
upload
</span>
<h3>Drag and drop a file or select add Image</h3>
</div>
</div>
</div>
<div class="file-upload-content">
<img class="file-upload-image" src="" alt="Uploaded Image" />
<div class="image-title-wrap">
<button type="button" onclick="removeUpload()" class="remove-image">Remove <span class="image-title">Uploaded Image</span></button>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="map" class="text-light">Map</label>
<div id="map" class="leaflet-container" style="max-width: 100%; height: 500px"></div>
</div>
<div class="form-group">
<label for="lat" class="text-light">Latitude</label>
<input type="text" id="lat" name="lat" placeholder="Latitude coordinates" class="form-control" required />
</div>
<div class="form-control">
<label for="long" class="text-light">Longitude</label>
<input type="text" id="long" name="long" placeholder="Longitude coordinates" class="form-control" required />
</div>
<button type="submit" class="ripple">UPLOAD</button>
</form>
</div>
</div>
<script>
</script>
<%- include('../partials/footer'); %>

24
views/partials/footer.ejs Normal file
View File

@ -0,0 +1,24 @@
<p class="text-center text-light">Source Code: <a href="https://github.com/moom0o/PhotoSphereStudio/">Github</a> - Made by <a href="https://github.com/moom0o">moom0o</a> & <a href="https://github.com/WinsDominoes">Win</a></p>
</div>
<script src="/assets/drag-and-drop.js"></script>
<!-- LEAFLET JS -->
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js" integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet.locatecontrol/dist/L.Control.Locate.min.js" charset="utf-8"></script>
<script src="/assets/leaflet/js/map.js"></script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<!-- Then Material JavaScript on top of Bootstrap JavaScript -->
<script src="/assets/material/material.min.js"></script>
</body>
</html>

42
views/partials/header.ejs Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSS -->
<link rel="stylesheet" href="/assets/style.css">
<!-- Add Material font (Roboto) and Material icon as needed -->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,500,500i,700,700i|Roboto+Mono:300,400,700|Roboto+Slab:300,400,700" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Add Material CSS, replace Bootstrap CSS -->
<link href="/assets/material/material.min.css" rel="stylesheet">
<!-- GOOGLE ICONS -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
<!-- LEAFLET FOR MAP -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet.locatecontrol/dist/L.Control.Locate.min.css" />
<title>PhotoSphereStudio - Upload 360° Photos to Google Maps</title>
</head>
<body class="bg-dark-2">
<header>
<nav class="navbar navbar-expand-lg navbar-dark bg-info">
<a class="navbar-brand" href="/">
<span>
<img src="/assets/icons/icon.png" alt="Icon" id="icon">
PhotoSphereStudio
</span></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</nav>
</header>
<div align="center">

View File

@ -1,25 +0,0 @@
<div style="text-align: center;"><h1>PhotoSphereStudio</h1>
<p>I am not very good at CSS, so please submit a pull request if you are great at frontend development.</p>
<p>This page will allow you to upload photo spheres to Google Maps after the idiots at Google removed their perfectly working StreetView app.</p>
<p><a href="https://github.com/moom0o/PhotoSphereStudio">Source Code</a></p>
<p>Images must have the proper 360 degree file properties, you will need to figure out how to add this metadata. Google pixel photosphere photos work perfectly, I have yet to figure out how to get my DJI drone 360 photos working.</p>
<p>Latitude and Longitude is not required if your photo already contains location data, however if it doesn't, then use <a href="https://www.latlong.net/">https://www.latlong.net/</a> to pinpoint coords.</p>
<form name="theform" action="/upload" method="POST" enctype="multipart/form-data">
<fieldset>
<legend>Upload a 360 image</legend>
<input type="file"
id="file" name="file"
accept="image/png, image/jpeg">
<div class="form-control">
<label for="lat">Latitude</label>
<input type="text" id="lat" name="lat" placeholder="Latitude coordinates" />
</div>
<div class="form-control">
<label for="long">Longitude</label>
<input type="text" id="long" name="long" placeholder="Longitude coordinates" />
</div>
<input type="submit" value="Send" class="submit-btn" />
</fieldset>
</form>
</div>