Some more openlayers improvments as well as initial work for projection support

This commit is contained in:
orangemug 2019-05-29 17:37:55 +01:00
parent c1cab38c7a
commit efe42021f1
9 changed files with 267 additions and 26 deletions

View file

@ -39,6 +39,7 @@
"maputnik-design": "github:maputnik/design", "maputnik-design": "github:maputnik/design",
"ol": "^6.0.0-beta.8", "ol": "^6.0.0-beta.8",
"ol-mapbox-style": "^5.0.0-beta.2", "ol-mapbox-style": "^5.0.0-beta.2",
"proj4": "^2.5.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.5.2", "react": "^16.5.2",
"react-aria-menubutton": "^6.0.1", "react-aria-menubutton": "^6.0.1",

View file

@ -218,6 +218,11 @@ export default class App extends React.Component {
showCollisionBoxes: false, showCollisionBoxes: false,
showOverdrawInspector: false, showOverdrawInspector: false,
}, },
openlayersDebugOptions: {
// TODO: For future projection work
// enableProjections: true,
debugToolbox: true,
},
} }
this.layerWatcher = new LayerWatcher({ this.layerWatcher = new LayerWatcher({
@ -478,10 +483,16 @@ export default class App extends React.Component {
return metadata['maputnik:renderer'] || 'mbgljs'; return metadata['maputnik:renderer'] || 'mbgljs';
} }
getProjectionCode () {
const metadata = this.state.mapStyle.metadata || {};
return this.state.openlayersDebugOptions.enableProjections ? metadata['maputnik:projection'] : "EPSG:3857";
}
mapRenderer() { mapRenderer() {
const metadata = this.state.mapStyle.metadata || {};
const mapProps = { const mapProps = {
mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}), mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
options: this.state.mapboxGlDebugOptions,
onDataChange: (e) => { onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map) this.layerWatcher.analyzeMap(e.map)
this.fetchSources(); this.fetchSources();
@ -496,9 +507,13 @@ export default class App extends React.Component {
if(renderer === 'ol') { if(renderer === 'ol') {
mapElement = <OpenLayersMap mapElement = <OpenLayersMap
{...mapProps} {...mapProps}
projectionCode={this.getProjectionCode()}
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
onLayerSelect={this.onLayerSelect}
/> />
} else { } else {
mapElement = <MapboxGlMap {...mapProps} mapElement = <MapboxGlMap {...mapProps}
options={this.state.mapboxGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"} inspectModeEnabled={this.state.mapState === "inspect"}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect} /> onLayerSelect={this.onLayerSelect} />
@ -540,6 +555,15 @@ export default class App extends React.Component {
this.setModal(modalName, !this.state.isOpen[modalName]); this.setModal(modalName, !this.state.isOpen[modalName]);
} }
onChangeOpenlayersDebug = (key, value) => {
this.setState({
openlayersDebugOptions: {
...this.state.openlayersDebugOptions,
[key]: value,
}
});
}
onChangeMaboxGlDebug = (key, value) => { onChangeMaboxGlDebug = (key, value) => {
this.setState({ this.setState({
mapboxGlDebugOptions: { mapboxGlDebugOptions: {
@ -555,6 +579,7 @@ export default class App extends React.Component {
const metadata = this.state.mapStyle.metadata || {} const metadata = this.state.mapStyle.metadata || {}
const toolbar = <Toolbar const toolbar = <Toolbar
renderer={this._getRenderer()}
mapState={this.state.mapState} mapState={this.state.mapState}
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
inspectModeEnabled={this.state.mapState === "inspect"} inspectModeEnabled={this.state.mapState === "inspect"}
@ -603,7 +628,9 @@ export default class App extends React.Component {
<DebugModal <DebugModal
renderer={this._getRenderer()} renderer={this._getRenderer()}
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions} mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
openlayersDebugOptions={this.state.openlayersDebugOptions}
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug} onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
isOpen={this.state.isOpen.debug} isOpen={this.state.isOpen.debug}
onOpenToggle={this.toggleModal.bind(this, 'debug')} onOpenToggle={this.toggleModal.bind(this, 'debug')}
/> />
@ -617,6 +644,7 @@ export default class App extends React.Component {
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.settings} isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')} onOpenToggle={this.toggleModal.bind(this, 'settings')}
openlayersDebugOptions={this.state.openlayersDebugOptions}
/> />
<ExportModal <ExportModal
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}

View file

@ -139,6 +139,7 @@ export default class Toolbar extends React.Component {
{ {
id: "inspect", id: "inspect",
title: "Inspect", title: "Inspect",
disabled: this.props.renderer !== 'mbgljs',
}, },
{ {
id: "filter-deuteranopia", id: "filter-deuteranopia",

View file

@ -34,6 +34,11 @@ class FeatureLayerPopup extends React.Component {
} }
_getFeatureColor(feature, zoom) { _getFeatureColor(feature, zoom) {
// Guard because openlayers won't have this
if (!feature.layer.paint) {
return;
}
try { try {
const paintProps = feature.layer.paint; const paintProps = feature.layer.paint;
let propName; let propName;
@ -105,11 +110,13 @@ class FeatureLayerPopup extends React.Component {
this.props.onLayerSelect(feature.layer.id) this.props.onLayerSelect(feature.layer.id)
}} }}
> >
{feature.layer.type &&
<LayerIcon type={feature.layer.type} style={{ <LayerIcon type={feature.layer.type} style={{
width: 14, width: 14,
height: 14, height: 14,
paddingRight: 3 paddingRight: 3
}}/> }}/>
}
{feature.layer.id} {feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>} {feature.counter && <span> × {feature.counter}</span>}
</label> </label>

View file

@ -3,10 +3,32 @@ import {throttle} from 'lodash';
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { loadJSON } from '../../libs/urlopen' import { loadJSON } from '../../libs/urlopen'
import FeatureLayerPopup from './FeatureLayerPopup';
import 'ol/ol.css' import 'ol/ol.css'
import {apply} from 'ol-mapbox-style'; import {apply} from 'ol-mapbox-style';
import {Map, View} from 'ol'; import {Map, View, Proj, Overlay} from 'ol';
import proj4 from 'proj4';
import {register} from 'ol/proj/proj4';
import {get as getProjection, toLonLat} from 'ol/proj';
import {toStringHDMS} from 'ol/coordinate';
// Register some projections...
proj4.defs('EPSG:3031', '+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs ');
register(proj4);
function renderCoords (coords) {
if (!coords || coords.length < 2) {
return null;
}
else {
return <span className="maputnik-coords">
{coords.map((coord) => String(coord).padStart(7, "\u00A0")).join(', ')}
</span>
}
}
export default class OpenLayersMap extends React.Component { export default class OpenLayersMap extends React.Component {
static propTypes = { static propTypes = {
@ -14,15 +36,23 @@ export default class OpenLayersMap extends React.Component {
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
accessToken: PropTypes.string, accessToken: PropTypes.string,
style: PropTypes.object, style: PropTypes.object,
onLayerSelect: PropTypes.func.isRequired,
} }
static defaultProps = { static defaultProps = {
onMapLoaded: () => {}, onMapLoaded: () => {},
onDataChange: () => {}, onDataChange: () => {},
onLayerSelect: () => {},
} }
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
zoom: 0,
rotation: 0,
cursor: [],
center: [],
};
this.updateStyle = throttle(this._updateStyle.bind(this), 200); this.updateStyle = throttle(this._updateStyle.bind(this), 200);
} }
@ -34,34 +64,126 @@ export default class OpenLayersMap extends React.Component {
apply(this.map, newMapStyle); apply(this.map, newMapStyle);
} }
componentDidUpdate() { updateProjection () {
this.projection = getProjection(this.props.projectionCode || "EPSG:3857");
}
componentDidUpdate(prevProps) {
if (this.props.projectionCode !== prevProps.projectionCode) {
this.updateProjection();
this.map.setView(
new View({
projection: this.projection,
zoom: 1,
center: [180, -90]
})
);
}
if (this.props.mapStyle !== prevProps.mapStyle) {
this.updateStyle(this.props.mapStyle); this.updateStyle(this.props.mapStyle);
} }
}
componentDidMount() { componentDidMount() {
this.updateStyle(this.props.mapStyle); this.overlay = new Overlay({
element: this.popupContainer,
autoPan: true,
autoPanAnimation: {
duration: 250
}
});
this.updateProjection();
const map = new Map({ const map = new Map({
target: this.container, target: this.container,
layers: [], overlays: [this.overlay],
view: new View({ view: new View({
zoom: 2, projection: this.projection,
center: [52.5, -78.4] zoom: 1,
center: [180, -90],
})
});
map.on('pointermove', (evt) => {
var coords = toLonLat(evt.coordinate, this.projection);
this.setState({
cursor: [
coords[0].toFixed(2),
coords[1].toFixed(2)
]
}) })
}) })
map.on('postrender', (evt) => {
const center = toLonLat(map.getView().getCenter(), this.projection);
this.setState({
center: [
center[0].toFixed(2),
center[1].toFixed(2),
],
rotation: map.getView().getRotation().toFixed(2),
zoom: map.getView().getZoom().toFixed(2)
});
});
this.map = map; this.map = map;
this.updateStyle(this.props.mapStyle);
}
closeOverlay = (e) => {
e.target.blur();
this.overlay.setPosition(undefined);
} }
render() { render() {
return <div return <div className="maputnik-ol-container">
<div
ref={x => this.popupContainer = x}
style={{background: "black"}}
className="maputnik-popup"
>
<button
class="mapboxgl-popup-close-button"
onClick={this.closeOverlay}
aria-label="Close popup"
>
×
</button>
<FeatureLayerPopup
features={this.state.selectedFeatures || []}
onLayerSelect={this.props.onLayerSelect}
/>
</div>
<div className="maputnik-ol-zoom">
Zoom level: {this.state.zoom}
</div>
{this.props.debugToolbox &&
<div className="maputnik-ol-debug">
<div>
<label>cursor: </label>
<span>{renderCoords(this.state.cursor)}</span>
</div>
<div>
<label>center: </label>
<span>{renderCoords(this.state.center)}</span>
</div>
<div>
<label>rotation: </label>
<span>{this.state.rotation}</span>
</div>
</div>
}
<div
className="maputnik-ol"
ref={x => this.container = x} ref={x => this.container = x}
style={{ style={{
width: "100%",
height: "100%",
backgroundColor: '#fff',
...this.props.style, ...this.props.style,
}}> }}>
</div> </div>
</div>
} }
} }

View file

@ -11,6 +11,7 @@ class DebugModal extends React.Component {
onChangeMaboxGlDebug: PropTypes.func.isRequired, onChangeMaboxGlDebug: PropTypes.func.isRequired,
onOpenToggle: PropTypes.func.isRequired, onOpenToggle: PropTypes.func.isRequired,
mapboxGlDebugOptions: PropTypes.object, mapboxGlDebugOptions: PropTypes.object,
openlayersDebugOptions: PropTypes.object,
} }
render() { render() {
@ -33,9 +34,15 @@ class DebugModal extends React.Component {
</ul> </ul>
} }
{this.props.renderer === 'ol' && {this.props.renderer === 'ol' &&
<div> <ul>
No debug options available for the OpenLayers renderer {Object.entries(this.props.openlayersDebugOptions).map(([key, val]) => {
</div> return <li key={key}>
<label>
<input type="checkbox" checked={val} onClick={(e) => this.props.onChangeOpenlayersDebug(key, e.target.checked)} /> {key}
</label>
</li>
})}
</ul>
} }
</div> </div>
</Modal> </Modal>

View file

@ -109,6 +109,21 @@ class SettingsModal extends React.Component {
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')} onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
/> />
</InputBlock> </InputBlock>
{this.props.openlayersDebugOptions.enableProjections &&
<InputBlock label={"Projection (experimental)"} doc={"Projection of the data"}>
<SelectInput {...inputProps}
data-wd-key="modal-settings.maputnik:projection"
options={[
['EPSG:3857', '[EPSG:3857] Web Mercator'],
['EPSG:3031', '[EPSG:3031] Antarctic Polar Stereographic (experimental)'],
]}
value={metadata['maputnik:projection'] || 'EPSG:3857'}
onChange={this.changeMetadataProperty.bind(this, 'maputnik:projection')}
/>
</InputBlock>
}
</div> </div>
</Modal> </Modal>
} }

View file

@ -1,7 +1,13 @@
//OPENLAYERS //OPENLAYERS
.maputnik-layout { .maputnik-layout {
.ol-zoom { .ol-zoom {
top: 10px; top: 40px;
right: 10px;
left: auto;
}
.ol-rotate {
top: 94px;
right: 10px; right: 10px;
left: auto; left: auto;
} }
@ -20,3 +26,57 @@
} }
} }
} }
.maputnik-ol {
width: 100%;
height: 100%;
}
.maputnik-ol-popup {
background: $color-black;
}
.maputnik-coords {
font-family: monospace;
&:before {
content: '[';
color: #888;
}
&:after {
content: ']';
color: #888;
}
}
.maputnik-ol-debug {
font-family: monospace;
font-size: smaller;
position: absolute;
bottom: 10px;
left: 10px;
background: rgb(28, 31, 36);
padding: 6px 8px;
border-radius: 2px;
z-indeX: 9999;
}
.maputnik-ol-zoom {
position: absolute;
right: 10px;
top: 10px;
background: #1c1f24;
border-radius: 2px;
padding: 6px 8px;
color: $color-lowgray;
z-indeX: 9999;
font-size: 12px;
font-weight: bold;
}
.maputnik-ol-container {
display: flex;
flex: 1;
position: relative;
}

View file

@ -22,7 +22,7 @@
.maputnik-popup-layer-id { .maputnik-popup-layer-id {
padding-left: $margin-2; padding-left: $margin-2;
padding-right: $margin-2; padding-right: 1.6em;
background-color: $color-midgray; background-color: $color-midgray;
color: $color-white; color: $color-white;
} }