Merged in feature/MAPG-97-integrate-leaflet (pull request #75)
Feature/MAPG-97 integrate leaflet
@ -5,3 +5,4 @@ DB_PASSWORD=mapguesser
|
||||
DB_NAME=mapguesser
|
||||
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
|
||||
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
|
||||
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
|
||||
|
@ -19,6 +19,10 @@ Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCol
|
||||
$routeCollection->get('position-json', '{mapId}/position.json', [MapGuesser\Controller\PositionController::class, 'getPosition']);
|
||||
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\PositionController::class, 'evaluateGuess']);
|
||||
});
|
||||
Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('admin.maps', 'maps', [MapGuesser\Controller\MapAdminController::class, 'getMaps']);
|
||||
$routeCollection->get('admin.mapEditor', 'mapEditor/{mapId}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']);
|
||||
});
|
||||
|
||||
$match = Container::$routeCollection->match($method, explode('/', $url));
|
||||
|
||||
|
1
public/static/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
190
public/static/css/game.css
Normal file
@ -0,0 +1,190 @@
|
||||
#roundInfo {
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#roundInfo p {
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
#panorama {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#cover {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#guess {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#guess.result {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#guess>#continueButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#closeGuessButtonContainer, #guess.result>#guessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#continueButtonContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#guess.result>#map {
|
||||
height: calc(100% - 170px);
|
||||
}
|
||||
|
||||
#resultInfo {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
padding: 5px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
background-color: #ffffff;
|
||||
border-radius: 3px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#resultInfo {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#resultInfo>div {
|
||||
width: 100%;
|
||||
height: 33.33%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#resultInfo p {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scoreBarBase {
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#scoreBar {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition-property: width;
|
||||
transition-duration: 2.0s;
|
||||
}
|
||||
|
||||
#showSummaryButton, #startNewGameButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
#showGuessButtonContainer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
#guess {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
opacity: 0.95;
|
||||
visibility: hidden;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 90px);
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
#showGuessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
#guess {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
#guess.adapt {
|
||||
top: initial;
|
||||
width: 250px;
|
||||
height: 200px;
|
||||
opacity: 0.5;
|
||||
transition-property: width, height, opacity;
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0.8s;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
#closeGuessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 45px);
|
||||
}
|
||||
#guess.result {
|
||||
width: initial;
|
||||
height: initial;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
right: 50px;
|
||||
bottom: 50px;
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 60%;
|
||||
}
|
||||
@media screen and (max-height: 424px) {
|
||||
#guess {
|
||||
top: 50px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
top: 50px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.result {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
49
public/static/css/map_editor.css
Normal file
@ -0,0 +1,49 @@
|
||||
#map {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#map.selected {
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
#panorama {
|
||||
width: 100%;
|
||||
height: calc(50% - 25px);
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#noPano {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: calc(50% - 25px);
|
||||
z-index: 2;
|
||||
visibility: hidden;
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
#noPano>p {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#control {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 60px;
|
||||
width: 125px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#placeControl {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: calc(50% + 35px);
|
||||
width: 100px;
|
||||
z-index: 3;
|
||||
visibility: hidden;
|
||||
}
|
@ -93,7 +93,10 @@ sub {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
svg.inline {
|
||||
svg.inline, img.inline {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
|
||||
@ -147,6 +150,14 @@ button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
button.red, a.button.red {
|
||||
background-color: #aa5e5e;
|
||||
}
|
||||
|
||||
button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
|
||||
background-color: #7f2929;
|
||||
}
|
||||
|
||||
div.header {
|
||||
background-color: #333333;
|
||||
height: 50px;
|
||||
@ -177,51 +188,6 @@ div.buttonContainer>button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
div.mapContainer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
div.mapItem {
|
||||
width: 350px;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
div.mapItem>div.title {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
div.mapItem>div.title>p.title {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
div.mapItem>img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
width: 64px;
|
||||
@ -234,147 +200,6 @@ div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#roundInfo {
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#roundInfo p {
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
#panorama {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#cover {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#guess {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#guess.result {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#guess>#continueButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#closeGuessButtonContainer, #guess.result>#guessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#continueButtonContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#guess.result>#map {
|
||||
height: calc(100% - 170px);
|
||||
}
|
||||
|
||||
#resultInfo {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
padding: 5px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
background-color: #ffffff;
|
||||
border-radius: 3px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#resultInfo {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#resultInfo>div {
|
||||
width: 100%;
|
||||
height: 33.33%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#resultInfo p {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scoreBarBase {
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#scoreBar {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition-property: width;
|
||||
transition-duration: 2.0s;
|
||||
}
|
||||
|
||||
#showSummaryButton, #startNewGameButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1504px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1134px) and (max-width: 1503px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 764px) and (max-width: 1133px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 763px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 374px) {
|
||||
div.mapItem {
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
div.header.small h1 span {
|
||||
display: none;
|
||||
@ -383,81 +208,4 @@ div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#showGuessButtonContainer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
#guess {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
opacity: 0.95;
|
||||
visibility: hidden;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 90px);
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
#showGuessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
#guess {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
#guess.adapt {
|
||||
top: initial;
|
||||
width: 250px;
|
||||
height: 200px;
|
||||
opacity: 0.5;
|
||||
transition-property: width, height, opacity;
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0.8s;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
#closeGuessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 45px);
|
||||
}
|
||||
#guess.result {
|
||||
width: initial;
|
||||
height: initial;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
right: 50px;
|
||||
bottom: 50px;
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 60%;
|
||||
}
|
||||
@media screen and (max-height: 424px) {
|
||||
#guess {
|
||||
top: 50px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
top: 50px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.result {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
74
public/static/css/maps.css
Normal file
@ -0,0 +1,74 @@
|
||||
#mapContainer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
div.mapItem {
|
||||
width: 350px;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
div.mapItem>div.title {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
div.mapItem>div.title>p.title {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
div.mapItem>img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1504px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1134px) and (max-width: 1503px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 764px) and (max-width: 1133px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 763px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 374px) {
|
||||
div.mapItem {
|
||||
width: initial;
|
||||
}
|
||||
}
|
2
public/static/img/favicon/SOURCE
Normal file
@ -0,0 +1,2 @@
|
||||
The PNGs in this folder are generated from '../icon.svg'.
|
||||
Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'.
|
@ -1,4 +1,3 @@
|
||||
<!-- The PNGs in this folder are generated from this SVG. -->
|
||||
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 16 16" fill="#28a745" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M15.817.613A.5.5 0 0 1 16 1v13a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 14.51l-4.902.98A.5.5 0 0 1 0 15V2a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0l4.902.98 4.902-.98a.5.5 0 0 1 .415.103zM10 2.41l-4-.8v11.98l4 .8V2.41zm1 11.98l4-.8V1.61l-4 .8v11.98zm-6-.8V1.61l-4 .8v11.98l4-.8z" />
|
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 529 B |
16
public/static/img/markers/marker-blue.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#3183ce"
|
||||
fill-rule="evenodd"
|
||||
stroke="#19456d"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#19456d"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
After Width: | Height: | Size: 639 B |
10
public/static/img/markers/marker-gray-empty.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#828282"
|
||||
fill-rule="evenodd"
|
||||
stroke="#383838"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
</svg>
|
After Width: | Height: | Size: 529 B |
16
public/static/img/markers/marker-gray.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#828282"
|
||||
fill-rule="evenodd"
|
||||
stroke="#383838"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#383838"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
After Width: | Height: | Size: 639 B |
10
public/static/img/markers/marker-green-empty.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#28a745"
|
||||
fill-rule="evenodd"
|
||||
stroke="#285624"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
</svg>
|
After Width: | Height: | Size: 529 B |
16
public/static/img/markers/marker-green.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#28a745"
|
||||
fill-rule="evenodd"
|
||||
stroke="#285624"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#285624"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
After Width: | Height: | Size: 639 B |
16
public/static/img/markers/marker-red.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#d24d4d"
|
||||
fill-rule="evenodd"
|
||||
stroke="#752929"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#752929"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
After Width: | Height: | Size: 639 B |
@ -222,7 +222,13 @@
|
||||
title: 'Open in Google Maps',
|
||||
zIndex: Core.rounds.length * 2,
|
||||
clickable: true,
|
||||
draggable: false
|
||||
draggable: false,
|
||||
icon: {
|
||||
url: '/static/img/markers/marker-green.svg',
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32)
|
||||
},
|
||||
});
|
||||
|
||||
round.realMarker.addListener('click', function () {
|
||||
@ -236,10 +242,17 @@
|
||||
zIndex: Core.rounds.length,
|
||||
clickable: false,
|
||||
draggable: false,
|
||||
icon: {
|
||||
url: '/static/img/markers/marker-gray-empty.svg',
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
labelOrigin: new google.maps.Point(12, 14)
|
||||
},
|
||||
label: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: '18px',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
text: '?'
|
||||
}
|
||||
@ -300,8 +313,15 @@
|
||||
for (var i = 0; i < Core.rounds.length; ++i) {
|
||||
var round = Core.rounds[i];
|
||||
|
||||
round.realMarker.setIcon({
|
||||
url: '/static/img/markers/marker-green-empty.svg',
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
labelOrigin: new google.maps.Point(12, 14)
|
||||
});
|
||||
round.realMarker.setLabel({
|
||||
color: '#812519',
|
||||
color: '#285624',
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
@ -385,10 +405,17 @@
|
||||
position: e.latLng,
|
||||
clickable: false,
|
||||
draggable: true,
|
||||
icon: {
|
||||
url: '/static/img/markers/marker-gray-empty.svg',
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
labelOrigin: new google.maps.Point(12, 14)
|
||||
},
|
||||
label: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: '18px',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
text: '?'
|
||||
}
|
||||
|
108
public/static/js/map_editor.js
Normal file
@ -0,0 +1,108 @@
|
||||
(function () {
|
||||
var MapEditor = {
|
||||
map: null,
|
||||
panorama: null,
|
||||
selectedMarker: null,
|
||||
|
||||
loadPano: function (data, status) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (status !== google.maps.StreetViewStatus.OK) {
|
||||
document.getElementById('noPano').style.visibility = 'visible';
|
||||
return;
|
||||
}
|
||||
|
||||
MapEditor.panorama.setVisible(true);
|
||||
MapEditor.panorama.setPov({ heading: 0, pitch: 0 });
|
||||
MapEditor.panorama.setZoom(0);
|
||||
MapEditor.panorama.setPano(data.location.pano);
|
||||
},
|
||||
|
||||
select: function (marker) {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
document.getElementById('map').classList.add('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
document.getElementById('panorama').style.display = 'block';
|
||||
document.getElementById('placeControl').style.visibility = 'visible';
|
||||
|
||||
MapEditor.resetSelected();
|
||||
MapEditor.selectedMarker = marker;
|
||||
|
||||
marker.setIcon(L.icon({
|
||||
iconUrl: '/static/img/markers/marker-blue.svg',
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}));
|
||||
marker.setZIndexOffset(2000);
|
||||
|
||||
MapEditor.map.invalidateSize(true);
|
||||
MapEditor.map.panTo(marker.getLatLng());
|
||||
|
||||
MapEditor.panorama.setVisible(false);
|
||||
|
||||
var sv = new google.maps.StreetViewService();
|
||||
sv.getPanorama({ location: marker.getLatLng(), preference: google.maps.StreetViewPreference.NEAREST, source: google.maps.StreetViewSource.OUTDOOR }, MapEditor.loadPano);
|
||||
},
|
||||
|
||||
resetSelected: function () {
|
||||
if (!MapEditor.selectedMarker) {
|
||||
return;
|
||||
}
|
||||
|
||||
MapEditor.selectedMarker.setIcon(L.icon({
|
||||
iconUrl: '/static/img/markers/marker-green.svg',
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}));
|
||||
MapEditor.selectedMarker.setZIndexOffset(1000);
|
||||
}
|
||||
};
|
||||
|
||||
MapEditor.map = L.map('map', {
|
||||
attributionControl: false,
|
||||
zoomControl: false
|
||||
});
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
minZoom: 0,
|
||||
maxZoom: 20
|
||||
}).addTo(MapEditor.map);
|
||||
|
||||
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
||||
|
||||
for (var i = 0; i < places.length; ++i) {
|
||||
var marker = L.marker({ lat: places[i].lat, lng: places[i].lng }, {
|
||||
icon: L.icon({
|
||||
iconUrl: '/static/img/markers/marker-green.svg',
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}),
|
||||
zIndexOffset: 1000
|
||||
})
|
||||
.addTo(MapEditor.map)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
}
|
||||
|
||||
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
|
||||
// switch off fullscreenControl because positioning doesn't work
|
||||
fullscreenControl: false,
|
||||
fullscreenControlOptions: {
|
||||
position: google.maps.ControlPosition.LEFT_TOP
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('cancelButton').onclick = function () {
|
||||
document.getElementById('map').classList.remove('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
document.getElementById('panorama').style.display = 'none';
|
||||
document.getElementById('placeControl').style.visibility = 'hidden';
|
||||
|
||||
MapEditor.resetSelected();
|
||||
MapEditor.selectedMarker = null;
|
||||
|
||||
MapEditor.map.invalidateSize(true);
|
||||
};
|
||||
})();
|
5
public/static/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"leaflet": "^1.6.0"
|
||||
}
|
||||
}
|
8
public/static/yarn.lock
Normal file
@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
leaflet@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
|
||||
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==
|
@ -9,13 +9,14 @@ if [ -f ${ROOT_DIR}/installed ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing MapGuesser DB..."
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Installing MapGuesser DB..."
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
fi
|
||||
|
||||
|
@ -4,6 +4,8 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
uglifyjs ${ROOT_DIR}/public/static/js/game.js -c -m -o ${ROOT_DIR}/public/static/js/game.js
|
||||
cleancss ${ROOT_DIR}/public/static/css/mapguesser.css -o ${ROOT_DIR}/public/static/css/mapguesser.css
|
||||
svgo ${ROOT_DIR}/public/static/img/loading.svg -o ${ROOT_DIR}/public/static/img/loading.svg
|
||||
find ${ROOT_DIR}/public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
|
||||
|
||||
find ${ROOT_DIR}/public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
|
||||
|
||||
find ${ROOT_DIR}/public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
|
||||
|
@ -7,8 +7,10 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
echo "Installing Composer packages..."
|
||||
(cd ${ROOT_DIR} && composer install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
fi
|
||||
|
53
src/Controller/MapAdminController.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
|
||||
class MapAdminController
|
||||
{
|
||||
public function getMaps(): IContent {
|
||||
//TODO
|
||||
|
||||
return new HtmlContent('admin/maps');
|
||||
}
|
||||
|
||||
public function getMapEditor(array $parameters): IContent {
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
|
||||
$bounds = $this->getMapBounds($mapId);
|
||||
|
||||
$places = $this->getPlaces($mapId);
|
||||
|
||||
$data = ['mapId' => $mapId, 'bounds' => $bounds->toArray(), 'places' => &$places];
|
||||
return new HtmlContent('admin/map_editor', $data);
|
||||
}
|
||||
|
||||
private function getMapBounds(int $mapId): Bounds
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
$select->whereId($mapId);
|
||||
|
||||
$map = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
|
||||
return $bounds;
|
||||
}
|
||||
|
||||
private function &getPlaces(int $mapId): array
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id', 'lat', 'lng']);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
$select->orderBy('lng');
|
||||
//$select->limit(100);
|
||||
|
||||
$places = $select->execute()->fetchAll(IResultSet::FETCH_ASSOC);
|
||||
|
||||
return $places;
|
||||
}
|
||||
}
|
27
views/admin/map_editor.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php $cssFiles = ['/static/node_modules/leaflet/dist/leaflet.css', '/static/css/map_editor.css']; ?>
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<?php require ROOT . '/views/templates/header.php'; ?>
|
||||
<div id="map"></div>
|
||||
<div id="control">
|
||||
<button id="saveButton" class="fullWidth">Save</button>
|
||||
<a class="button gray fullWidth marginTop" href="/admin/maps" title="Back to maps">Back to maps</a>
|
||||
</div>
|
||||
<div id="panorama"></div>
|
||||
<div id="noPano">
|
||||
<p class="bold">No panorama is available for this location.</p>
|
||||
</div>
|
||||
<div id="placeControl">
|
||||
<button id="applyButton" class="fullWidth">Apply</button>
|
||||
<button id="cancelButton" class="gray fullWidth marginTop">Cancel</button>
|
||||
<button id="deleteButton" class="red fullWidth marginTop">Delete</button>
|
||||
</div>
|
||||
<script>
|
||||
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
|
||||
var mapId = '<?= $mapId ?>';
|
||||
var mapBounds = <?= json_encode($bounds) ?>;
|
||||
var places = <?= json_encode($places) ?>;
|
||||
</script>
|
||||
<script src="/static/node_modules/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>"></script>
|
||||
<script src="/static/js/map_editor.js"></script>
|
||||
<?php require ROOT . '/views/templates/main_footer.php'; ?>
|
7
views/admin/maps.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<?php require ROOT . '/views/templates/header.php'; ?>
|
||||
<div class="main">
|
||||
<h2>Maps</h2>
|
||||
<p>TODO</p>
|
||||
</div>
|
||||
<?php require ROOT . '/views/templates/main_footer.php'; ?>
|
@ -1,9 +1,10 @@
|
||||
<?php $cssFiles = ['/static/css/game.css']; ?>
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<div class="header small">
|
||||
<div class="grid">
|
||||
<h1>
|
||||
<a href="/maps" title="Back to playable maps">
|
||||
<?php require ROOT . '/views/templates/icon.php'; ?>
|
||||
<img class="inline" src="/static/img/icon.svg">
|
||||
<span>MapGuesser</span>
|
||||
</a>
|
||||
</h1>
|
||||
|
@ -1,8 +1,9 @@
|
||||
<?php $cssFiles = ['/static/css/maps.css']; ?>
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<?php require ROOT . '/views/templates/header.php'; ?>
|
||||
<div class="main">
|
||||
<h2>Playable maps</h2>
|
||||
<div class="mapContainer">
|
||||
<div id="mapContainer">
|
||||
<?php foreach ($maps as $map) : ?>
|
||||
<div class="mapItem">
|
||||
<div class="title">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="header">
|
||||
<h1>
|
||||
<a href="/" title="MapGuesser">
|
||||
<?php require ROOT . '/views/templates/icon.php'; ?>
|
||||
<img class="inline" src="/static/img/icon.svg">
|
||||
MapGuesser
|
||||
</a>
|
||||
</h1>
|
||||
|
@ -1,4 +0,0 @@
|
||||
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
|
||||
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="#28a745" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M15.817.613A.5.5 0 0 1 16 1v13a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 14.51l-4.902.98A.5.5 0 0 1 0 15V2a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0l4.902.98 4.902-.98a.5.5 0 0 1 .415.103zM10 2.41l-4-.8v11.98l4 .8V2.41zm1 11.98l4-.8V1.61l-4 .8v11.98zm-6-.8V1.61l-4 .8v11.98l4-.8z" />
|
||||
</svg>
|
@ -5,6 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MapGuesser</title>
|
||||
<link href="/static/css/mapguesser.css" rel="stylesheet">
|
||||
<?php if (isset($cssFiles)) : ?>
|
||||
<?php foreach ($cssFiles as $cssFile) : ?>
|
||||
<link href="<?= $cssFile ?>" rel="stylesheet">
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&family=Roboto+Mono:wght@300;500&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/static/img/favicon/192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/img/favicon/96x96.png">
|
||||
|