Merge remote-tracking branch 'upstream/master' into feature/add-range-slider

This commit is contained in:
orangemug
2019-05-19 06:04:21 +01:00
30 changed files with 1523 additions and 1468 deletions

View File

@@ -49,6 +49,26 @@ function normalizeSourceURL (url, apiToken="") {
}
}
function setFetchAccessToken(url, mapStyle) {
const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/);
if (matchesTilehosting || matchesMaptiler) {
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else if (matchesThunderforest) {
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else {
return url;
}
}
function updateRootSpec(spec, fieldName, newValues) {
return {
@@ -73,57 +93,48 @@ export default class App extends React.Component {
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
})
const keyCodes = {
"esc": 27,
"?": 191,
"o": 79,
"e": 69,
"s": 83,
"d": 68,
"i": 73,
"m": 77,
}
const shortcuts = [
{
keyCode: keyCodes["?"],
key: "?",
handler: () => {
this.toggleModal("shortcuts");
}
},
{
keyCode: keyCodes["o"],
key: "o",
handler: () => {
this.toggleModal("open");
}
},
{
keyCode: keyCodes["e"],
key: "e",
handler: () => {
this.toggleModal("export");
}
},
{
keyCode: keyCodes["d"],
key: "d",
handler: () => {
this.toggleModal("sources");
}
},
{
keyCode: keyCodes["s"],
key: "s",
handler: () => {
this.toggleModal("settings");
}
},
{
keyCode: keyCodes["i"],
key: "i",
handler: () => {
this.setMapState("inspect");
this.setMapState(
this.state.mapState === "map" ? "inspect" : "map"
);
}
},
{
keyCode: keyCodes["m"],
key: "m",
handler: () => {
document.querySelector(".mapboxgl-canvas").focus();
}
@@ -131,16 +142,17 @@ export default class App extends React.Component {
]
document.body.addEventListener("keyup", (e) => {
if(e.keyCode === keyCodes["esc"]) {
if(e.key === "Escape") {
e.target.blur();
document.body.focus();
}
else if(document.activeElement === document.body) {
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
const shortcut = shortcuts.find((shortcut) => {
return (shortcut.keyCode === e.keyCode)
return (shortcut.key === e.key)
})
if(shortcut) {
this.setModal("shortcuts", false);
shortcut.handler(e);
}
}
@@ -195,7 +207,8 @@ export default class App extends React.Component {
},
mapOptions: {
showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries"),
showCollisionBoxes: queryUtil.asBool(queryObj, "show-collision-boxes")
showCollisionBoxes: queryUtil.asBool(queryObj, "show-collision-boxes"),
showOverdrawInspector: queryUtil.asBool(queryObj, "show-overdraw-inspector")
},
}
@@ -405,6 +418,12 @@ export default class App extends React.Component {
console.warn("Failed to normalizeSourceURL: ", err);
}
try {
url = setFetchAccessToken(url, this.state.mapStyle)
} catch(err) {
console.warn("Failed to setFetchAccessToken: ", err);
}
fetch(url, {
mode: 'cors',
})
@@ -488,17 +507,21 @@ export default class App extends React.Component {
this.setState({ selectedLayerIndex: idx })
}
toggleModal(modalName) {
setModal(modalName, value) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({
isOpen: {
...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName]
[modalName]: value
}
})
}
if(modalName === 'survey') {
localStorage.setItem('survey', '');
}
toggleModal(modalName) {
this.setModal(modalName, !this.state.isOpen[modalName]);
}
render() {
@@ -553,6 +576,7 @@ export default class App extends React.Component {
const modals = <div>
<ShortcutsModal
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>

View File

@@ -198,7 +198,7 @@ export default class Toolbar extends React.Component {
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<IconText>View </IconText>
<select onChange={(e) => this.handleSelection(e.target.value)}>
<select onChange={(e) => this.handleSelection(e.target.value)} value={currentView.id}>
{views.map((item) => {
return (
<option key={item.id} value={item.id}>

View File

@@ -81,6 +81,7 @@ export default class SpecField extends React.Component {
options={options}
/>
}
case 'formatted':
case 'string':
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
return <IconInput

View File

@@ -54,7 +54,8 @@ class AutocompleteInput extends React.Component {
menuStyle={{
position: "fixed",
overflow: "auto",
maxHeight: this.state.maxHeight
maxHeight: this.state.maxHeight,
zIndex: '998'
}}
wrapperProps={{
className: "maputnik-autocomplete",

View File

@@ -5,7 +5,7 @@ import AutocompleteInput from './AutocompleteInput'
class IconInput extends React.Component {
static propTypes = {
value: PropTypes.array,
value: PropTypes.string,
icons: PropTypes.array,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,

View File

@@ -29,20 +29,11 @@ class JSONEditor extends React.Component {
}
}
static getDerivedStateFromProps(props, state) {
return {
code: JSON.stringify(props.layer, null, 2)
};
}
shouldComponentUpdate(nextProps, nextState) {
try {
const parsedLayer = JSON.parse(this.state.code)
// If the structure is still the same do not update
// because it affects editing experience by reformatting all the time
return nextState.code !== JSON.stringify(parsedLayer, null, 2)
} catch(err) {
return true
componentDidUpdate(prevProps) {
if (prevProps.layer !== this.props.layer) {
this.setState({
code: JSON.stringify(this.props.layer, null, 2)
})
}
}

View File

@@ -46,7 +46,7 @@ class LayerListContainer extends React.Component {
areAllGroupsExpanded: false,
isOpen: {
add: false,
}
}
}
toggleModal(modalName) {
@@ -66,12 +66,12 @@ class LayerListContainer extends React.Component {
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
const lookupKey = [groupPrefix, idx].join('-')
if (layers.length > 1) {
newGroups[lookupKey] = this.state.areAllGroupsExpanded
}
layers.forEach((layer) => {
idx += 1
})
@@ -204,6 +204,7 @@ export default class LayerList extends React.Component {
render() {
return <LayerListContainerSortable
{...this.props}
helperClass='sortableHelper'
onSortEnd={this.props.onMoveLayer.bind(this)}
useDragHandle={true}
/>

View File

@@ -7,23 +7,16 @@ import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icon
import LayerIcon from '../icons/LayerIcon'
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
class LayerTypeDragHandle extends React.Component {
static propTypes = LayerIcon.propTypes
render() {
return <LayerIcon
{...this.props}
style={{
cursor: 'move',
width: 14,
height: 14,
paddingRight: 3,
}}
const DraggableLabel = SortableHandle((props) => {
return <div className="maputnik-layer-list-item-handle">
<LayerIcon
className="layer-handle__icon"
type={props.layerType}
/>
}
}
const LayerTypeDragHandleSortable = SortableHandle((props) => <LayerTypeDragHandle {...props} />)
<span className="maputnik-layer-list-item-id">{props.layerId}</span>
</div>
});
class IconAction extends React.Component {
static propTypes = {
@@ -111,8 +104,7 @@ class LayerListItem extends React.Component {
"maputnik-layer-list-item-selected": this.props.isSelected,
[this.props.className]: true,
})}>
<LayerTypeDragHandleSortable type={this.props.layerType} />
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
<DraggableLabel {...this.props} />
<span style={{flexGrow: 1}} />
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":delete"}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import LayerIcon from '../icons/LayerIcon'
import {latest, expression, function as styleFunction} from '@mapbox/mapbox-gl-style-spec'
function groupFeaturesBySourceLayer(features) {
const sources = {}
@@ -28,7 +29,59 @@ function groupFeaturesBySourceLayer(features) {
class FeatureLayerPopup extends React.Component {
static propTypes = {
onLayerSelect: PropTypes.func.isRequired,
features: PropTypes.array
features: PropTypes.array,
zoom: PropTypes.number,
}
_getFeatureColor(feature, zoom) {
try {
const paintProps = feature.layer.paint;
let propName;
if(paintProps.hasOwnProperty("text-color") && paintProps["text-color"]) {
propName = "text-color";
}
else if (paintProps.hasOwnProperty("fill-color") && paintProps["fill-color"]) {
propName = "fill-color";
}
else if (paintProps.hasOwnProperty("line-color") && paintProps["line-color"]) {
propName = "line-color";
}
else if (paintProps.hasOwnProperty("fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
propName = "fill-extrusion-color";
}
if(propName) {
const propertySpec = latest["paint_"+feature.layer.type][propName];
let color = feature.layer.paint[propName];
if(typeof(color) === "object") {
if(color.stops) {
color = styleFunction.convertFunction(color, propertySpec);
}
const exprResult = expression.createExpression(color, propertySpec);
const val = exprResult.value.evaluate({
zoom: zoom
}, feature);
return val.toString();
}
else {
return color;
}
}
else {
// Default color
return "black";
}
}
// This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error.
catch (err) {
console.error("Unable to get feature color, error:", err);
return "black";
}
}
render() {
@@ -36,21 +89,31 @@ class FeatureLayerPopup extends React.Component {
const items = Object.keys(sources).map(vectorLayerId => {
const layers = sources[vectorLayerId].map((feature, idx) => {
return <label
key={idx}
className="maputnik-popup-layer"
onClick={() => {
this.props.onLayerSelect(feature.layer.id)
}}
const featureColor = this._getFeatureColor(feature, this.props.zoom);
return <div
key={idx}
className="maputnik-popup-layer"
>
<LayerIcon type={feature.layer.type} style={{
width: 14,
height: 14,
paddingRight: 3
}}/>
{feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>}
</label>
<div
className="maputnik-popup-layer__swatch"
style={{background: featureColor}}
></div>
<label
className="maputnik-popup-layer__label"
onClick={() => {
this.props.onLayerSelect(feature.layer.id)
}}
>
<LayerIcon type={feature.layer.type} style={{
width: 14,
height: 14,
paddingRight: 3
}}/>
{feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>}
</label>
</div>
})
return <div key={vectorLayerId}>
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>

View File

@@ -22,7 +22,7 @@ function renderProperties(feature) {
}
function renderFeature(feature) {
return <div key={feature.id}>
return <div key={`${feature.sourceLayer}-${feature.id}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<InputBlock key={"property-type"} label={"$type"}>
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
@@ -43,7 +43,7 @@ function removeDuplicatedFeatures(features) {
if(featureIndex === -1) {
uniqueFeatures.push(feature)
} else {
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
if(uniqueFeatures[featureIndex].hasOwnProperty('inspectModeCounter')) {
uniqueFeatures[featureIndex].inspectModeCounter++
} else {
uniqueFeatures[featureIndex].inspectModeCounter = 2

View File

@@ -17,10 +17,10 @@ import '../../libs/mapbox-rtl'
const IS_SUPPORTED = MapboxGl.supported();
function renderPropertyPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
return mountNode.innerHTML;
function renderPopup(popup, mountNode) {
ReactDOM.render(popup, mountNode);
var content = mountNode.innerHTML;
return content;
}
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
@@ -77,9 +77,6 @@ export default class MapboxGlMap extends React.Component {
this.state = {
map: null,
inspect: null,
isPopupOpen: false,
popupX: 0,
popupY: 0,
}
}
@@ -97,6 +94,16 @@ export default class MapboxGlMap extends React.Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
} catch(e) {
// no biggie, carry on
}
return should;
}
componentDidUpdate(prevProps) {
if(!IS_SUPPORTED) return;
@@ -114,6 +121,7 @@ export default class MapboxGlMap extends React.Component {
if (map) {
map.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
}
}
@@ -131,6 +139,7 @@ export default class MapboxGlMap extends React.Component {
map.showTileBoundaries = mapOpts.showTileBoundaries;
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
const zoom = new ZoomControl;
map.addControl(zoom, 'top-right');
@@ -138,6 +147,8 @@ export default class MapboxGlMap extends React.Component {
const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right');
const tmpNode = document.createElement('div');
const inspect = new MapboxInspect({
popup: new MapboxGl.Popup({
closeOnClick: false
@@ -153,18 +164,23 @@ export default class MapboxGlMap extends React.Component {
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: features => {
if(this.props.inspectModeEnabled) {
return renderPropertyPopup(features)
return renderPopup(<FeaturePropertyPopup features={features} />, tmpNode);
} else {
var mountNode = document.createElement('div');
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
return mountNode
return renderPopup(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} zoom={this.state.zoom} />, tmpNode);
}
}
})
map.addControl(inspect)
map.on("style.load", () => {
this.setState({ map, inspect });
this.setState({
map,
inspect,
zoom: map.getZoom()
});
if(this.props.inspectModeEnabled) {
inspect.toggleInspector();
}
})
map.on("data", e => {
@@ -173,6 +189,12 @@ export default class MapboxGlMap extends React.Component {
map: this.state.map
})
})
map.on("zoom", e => {
this.setState({
zoom: map.getZoom()
});
})
}
render() {