Commit df29f729 authored by alain's avatar alain 🐙

add modal, add location select, add map controls, etc

parent dc0a68ca
......@@ -3,13 +3,12 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@math.gl/web-mercator": "^3.2.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-map-gl": "^5.2.8",
"react-scripts": "3.4.3"
},
"devDependencies": {
"@rescripts/cli": "0.0.14",
......
......@@ -10,6 +10,15 @@
<title>Hollandse Luchten Route</title>
</head>
<body>
<style>
body {
font-family: 'Maax', sans-serif;
margin: 0;
}
h1 {
font-weight: 500;
}
</style>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
......
import React, { useState } from 'react';
import ReactMapGL, { Marker, Source, Layer, GeolocateControl } from 'react-map-gl';
import React, { useState, useEffect } from 'react';
import ReactMapGL, { Marker, Source, Layer, FlyToInterpolator } from 'react-map-gl';
import WebMercatorViewport from '@math.gl/web-mercator';
import { mapStyle } from "./mapStyle"
import { mapControlSettings, initialViewport } from "./mapSettings"
import MapControls from './MapControls'
import MapLocations from "./MapLocations"
import Modal from './Modal'
import { routeData } from './routeData'
import { routePoints } from './routePoints'
import { routeStyle } from './routeStyle'
function App() {
const [viewport, setViewport] = useState({
latitude: 52.4934,
longitude: 4.5969,
zoom: 14,
pitch: 60
});
const onViewportChange = viewport => {
const {width, height, ...etc} = viewport
setViewport({...etc})
}
const [viewport, setViewport] = useState(initialViewport);
const [modalContent, setModalContent] = useState(null);
const [selectedCenter, setSelectedCenter] = useState(null);
const routeData = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"shape": "Line",
},
"geometry": {
"type": "LineString",
"coordinates": [
[4.600461, 52.494056],
[4.59368, 52.494657],
[4.593487, 52.494304],
[4.593573, 52.494082],
[4.593701, 52.493834],
[4.594238, 52.493521],
[4.593294, 52.492528],
[4.592843, 52.492306],
[4.591985, 52.492462],
[4.591277, 52.491992],
[4.589539, 52.492253]
]
}
}]
}
useEffect(() => {
const point = window.location.hash ? routePoints[window.location.hash.replace('#', '')] : routePoints[0]
const routeStyle = {
paint: {
'line-color': '#2FB6BC',
'line-width': 3
if(point) {
setModalContent(point)
}
};
const points = [
{
title: "Meetstation",
latitude: 52.493990,
longitude: 4.602379
},
{
title: "Moriaan",
latitude: 52.4941078,
longitude: 4.594586
}, [])
const changeViewport = (event, change) => {
event.stopPropagation()
setViewport({...viewport, ...change })
}
const changeViewportBounds = (event, bounds) => {
if(event) event.stopPropagation()
const { longitude, latitude, zoom } = new WebMercatorViewport(viewport).fitBounds(bounds)
const change = {
longitude,
latitude,
zoom,
transitionDuration: 500,
transitionInterpolator: new FlyToInterpolator()
}
]
setViewport({...viewport, ...change })
setSelectedCenter([longitude, latitude])
}
// const changeViewportCenter = (event, center, zoom) => {
// if(event) event.stopPropagation()
// const { viewport } = this.state
// const change = {
// longitude: center[0],
// latitude: center[1],
// zoom,
// transitionDuration: 500,
// transitionInterpolator: new FlyToInterpolator()
// }
// this.setState({
// viewport: {...viewport, ...change },
// selectedCenter: center,
// })
// }
const closeModal = (e) => {
setModalContent(null)
}
return (
<ReactMapGL width="100%" height="100%" {...viewport} onViewportChange={viewport => onViewportChange(viewport)} mapStyle={mapStyle}>
<div key="5" className='mapboxgl-ctrl-top-left'>
<GeolocateControl positionOptions={{enableHighAccuracy: true}} trackUserLocation={true} />
</div>
<Source type="geojson" data={routeData}>
<Layer id="route" type="line" {...routeStyle} />
</Source>
{ points.map(point => (
<Marker key={point.title} latitude={point.latitude} longitude={point.longitude} offsetLeft={-10} offsetTop={-20}>
<h3>{ point.title }</h3>
</Marker>
)) }
</ReactMapGL>
<>
<ReactMapGL width="100%" height="100%" {...viewport} onViewportChange={viewport => setViewport(viewport)} mapStyle={mapStyle}>
<MapControls mapControlSettings={mapControlSettings} viewport={viewport} changeViewport={changeViewport} />
<div className='mapboxgl-ctrl-top-left'>
{ routePoints && <MapLocations mapLocations={routePoints} changeViewportBounds={changeViewportBounds} selectedCenter={selectedCenter} /> }
</div>
<Source type="geojson" data={routeData}>
<Layer id="route" type="line" {...routeStyle} />
</Source>
{ routePoints.filter(point => point.latitude).map(point => (
<Marker className="route-marker" key={point.title} latitude={point.latitude} longitude={point.longitude} offsetLeft={-10} offsetTop={-20}>
<h3 onClick={ e => { setModalContent(point) } }>{ point.title }</h3>
</Marker>
)) }
</ReactMapGL>
{ modalContent && <Modal closeModal={closeModal}>
<h1>{modalContent.title}</h1>
<div dangerouslySetInnerHTML={{ __html: modalContent.body}} />
</Modal> }
</>
);
}
......
import React from 'react'
const IconArrowDown = () => {
return (
<svg className="icon icon-ui icon-arrow-down" width="14" height="14" viewBox="0 0 14 14">
<polyline fill="none" stroke="#000" strokeWidth="2" strokeMiterlimit="10" points="12,6 7,10 2,6 "></polyline>
</svg>
)
}
export default IconArrowDown
\ No newline at end of file
import React from 'react'
import { GeolocateControl } from 'react-map-gl';
class MapControls extends React.Component {
constructor(props){
super()
this.state = {
}
}
render() {
const viewport = this.props.viewport
const changeViewport = this.props.changeViewport
const { transitionDuration, zoomStep, pitchStep, pitchMax, bearingStep } = this.props.mapControlSettings
return (
<div className='mapboxgl-ctrl-top-right'>
<div className="map-crtl-group">
<svg className="map-ctrl-button-square" width="40" height="40" viewBox="0 0 40 40">
<g id="zoom-in" onClick={(event) => changeViewport(event, { zoom: viewport.zoom + zoomStep, transitionDuration })}>
<rect width="40" height="40" fill="#fff"/><path className="stroke" d="M13,20H27"/><path className="stroke" d="M20,27V13"/>
</g>
</svg>
<svg className="map-ctrl-button-square" width="40" height="40" viewBox="0 0 40 40">
<g id="pitch-up" className={(viewport.pitch >= pitchMax ? "disabled" : "")} onClick={(event) => changeViewport(event, { pitch: viewport.pitch + pitchStep, transitionDuration })}>
<path d="M0,0H40L20,20Z" fill="#fff"/><path className="stroke" d="M24.561,9.561,20,5,15.439,9.561"/>
</g>
<g id="pitch-down" className={(viewport.pitch <= 0 ? "disabled" : "")} onClick={(event) => changeViewport(event, { pitch: viewport.pitch - pitchStep, transitionDuration })}>
<path d="M40,40H0L20,20Z" fill="#fff"/><path className="stroke" d="M15.439,30.439,20,35l4.561-4.561"/>
</g>
<g id="bearing-left" onClick={(event) => changeViewport(event, { bearing: viewport.bearing + bearingStep, transitionDuration })}>
<path d="M0,40V0L20,20Z" fill="#fff"/><path className="stroke" d="M9.561,15.439,5,20l4.561,4.561"/>
</g>
<g id="bearing-right" onClick={(event) => changeViewport(event, { bearing: viewport.bearing - bearingStep, transitionDuration })}>
<path d="M40,0V40L20,20Z" fill="#fff"/><path className="stroke" d="M30.439,24.561,35,20l-4.561-4.561"/>
</g>
</svg>
<svg className="map-ctrl-button-square" width="40" height="40" viewBox="0 0 40 40">
<g id="zoom-out" onClick={(event) => changeViewport(event, { zoom: viewport.zoom - zoomStep, transitionDuration })}>
<rect width="40" height="40" fill="#fff"/><path className="stroke" d="M13,20H27"/>
</g>
</svg>
<GeolocateControl positionOptions={{enableHighAccuracy: true}} trackUserLocation={true} />
</div>
</div>
)
}
}
export default MapControls
\ No newline at end of file
import React from 'react'
//import { connect } from 'react-redux'
//import { setPanelStatus } from "../actions"
import IconArrowDown from "./IconArrowDown"
class MapLocations extends React.Component {
constructor(props){
super()
this.state = {
open: false,
current: "Ga naar...",
selectedCenter: props.selectedCenter
}
}
static getDerivedStateFromProps(props, state) {
if (props.selectedCenter !== state.selectedCenter) {
if(props.selectedCenter === null) {
return {
current: "Ga naar...",
selectedCenter: props.selectedCenter
}
} else {
return {
selectedCenter: props.selectedCenter
}
}
}
return null
}
addListOptions = (locations, sub, options = []) => {
for (let i = 0; i < locations.length; i++) {
options.push({
title: locations[i].title,
bounds: locations[i].bounds,
sub: sub
})
if(locations[i].sublocations) {
this.addListOptions(locations[i].sublocations, true, options)
}
}
return options
}
render() {
const { mapLocations, changeViewportBounds } = this.props
const { open, current } = this.state
const mapLocationsFlat = this.addListOptions(mapLocations, false)
const listOptions = mapLocationsFlat.map(location => {
const { title, bounds, sub } = location
const classNames = []
if (current === title) classNames.push("selected")
if (sub) classNames.push("sub")
return (
<li key={title} className={ classNames.join(" ") } onClick={(event) => {
changeViewportBounds(event, bounds)
this.setState({ current: title, open: false })
}}>{title}</li>
)
})
return (
<div id="map-locations" className={(open ? "select open" : "select")}>
<IconArrowDown />
<div className="value-current" onClick={() => { this.setState({ open: !open }) }}>{current}</div>
<ul className="value-list">
{listOptions}
</ul>
</div>
)
}
}
// const mapStateToProps = (state) => ({
// //infoBox: state.infoBox
// })
// MapLocations = connect(mapStateToProps)(MapLocations)
export default MapLocations
\ No newline at end of file
import React from "react";
class Modal extends React.Component {
constructor(props) {
super()
this.state = {}
this.handleKeydown = this.handleKeydown.bind(this)
}
handleKeydown(event){
if(event.keyCode === 27) {
this.props.closeModal()
}
}
componentDidMount(){
this.mountTime = + new Date()
document.addEventListener("keydown", this.handleKeydown, false)
}
componentWillUnmount(){
document.removeEventListener("keydown", this.handleKeydown, false)
}
render() {
const closeModal = this.props.closeModal
return (
<div className="modal" onClick={event => {
// Prevent closing on double click / touch devices (it's registering a click event for some reason..)
if(+ new Date() - this.mountTime > 300) closeModal(event)
}}>
<div className="content" onClick={event => { event.stopPropagation() }}>
<button className="button-close" onClick={event => closeModal(event)}>×</button>
{this.props.children}
</div>
</div>
)
}
}
export default Modal
\ No newline at end of file
.modal {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.15);
overflow: auto;
text-align: center;
z-index: 1200;
&:before {
content: "";
display: inline-block;
height: 100%;
vertical-align: middle;
}
.content {
display: inline-block;
vertical-align: middle;
position: relative;
background-color: #FFF;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
padding: 20px;
width: 100%;
max-width: 950px;
min-height: 1rem;
text-align: left;
}
.button-close {
border: none;
background: none !important;
box-shadow: none;
color: #000;
font-size: 20px;
position: absolute;
top: 10px;
right: 10px;
padding: 0;
width: 30px;
line-height: 30px;
cursor: pointer;
transition: all 0ms ease;
&:hover {
font-size: 24px;
transition: all 100ms ease;
}
&:active {
outline: none;
}
&:after {
content: none;
}
}
h1 {
padding-right: 1rem;
}
.description {
margin-bottom: 32px;
}
}
\ No newline at end of file
* {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
margin: 0;
}
ul,
ol {
margin: 0;
padding: 0;
list-style: none;
}
table {
border-collapse: collapse;
}
svg {
display: block;
}
.mapboxgl-map + footer {
margin-top: 0;
}
input,
button {
&:focus {
outline: none;
}
}
\ No newline at end of file
@import "reset";
#root {
@import "modal";
height: 100vh;
h3 {
font-weight: 500;
}
#map-locations {
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.15);
min-width: 160px;
}
.route-marker {
background-color: #FFF;
padding: 0.2em 0.4em;
white-space: nowrap;
box-shadow: 0 0 0.4rem 0.1rem rgba(0, 0, 0, 0.15);
}
.route-marker:after {
content: "";
position: absolute;
top: 100%;
left: 4px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #FFF;
}
.mapboxgl-marker h3 {
margin: 0;
}
.map-crtl-group {
margin: 10px;
}
.mapboxgl-ctrl {
user-select: none;
}
@media (max-width: 700px) {
.mapboxgl-ctrl-top-right {
top: 50%;
transform: translateY(-50%);
}
}
.mapboxgl-ctrl-group {
box-shadow: none;
border-radius: 0;
}
.mapboxgl-ctrl-group button {
width: 34px;
height: 34px;
background-color: #FFF;
border-radius: 0;
box-shadow: none;
.mapboxgl-ctrl-icon {
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.15);
border-radius: 0;
outline: none;
}
}
.mapboxgl-ctrl-group {
background-color: transparent;
}
button.mapboxgl-ctrl-icon {
background-color: white !important;
&:after {
content: none;