import { noTarget, isOfMetaType, isActiveFeature, isInactiveFeature, isShiftDown } from '../lib/common_selectors.js'; import createSupplementaryPoints from '../lib/create_supplementary_points.js'; import constrainFeatureMovement from '../lib/constrain_feature_movement.js'; import doubleClickZoom from '../lib/double_click_zoom.js'; import * as Constants from '../constants.js'; import moveFeatures from '../lib/move_features.js'; const isVertex = isOfMetaType(Constants.meta.VERTEX); const isMidpoint = isOfMetaType(Constants.meta.MIDPOINT); const DirectSelect = {}; // INTERNAL FUCNTIONS DirectSelect.fireUpdate = function() { this.fire(Constants.events.UPDATE, { action: Constants.updateActions.CHANGE_COORDINATES, features: this.getSelected().map(f => f.toGeoJSON()) }); }; DirectSelect.fireActionable = function(state) { this.setActionableState({ combineFeatures: false, uncombineFeatures: false, trash: state.selectedCoordPaths.length > 0 }); }; DirectSelect.startDragging = function(state, e) { state.initialDragPanState = this.map.dragPan.isEnabled(); this.map.dragPan.disable(); state.canDragMove = true; state.dragMoveLocation = e.lngLat; }; DirectSelect.stopDragging = function(state) { if (state.canDragMove && state.initialDragPanState === true) { this.map.dragPan.enable(); } state.dragMoving = false; state.canDragMove = false; state.dragMoveLocation = null; }; DirectSelect.onVertex = function (state, e) { this.startDragging(state, e); const about = e.featureTarget.properties; const selectedIndex = state.selectedCoordPaths.indexOf(about.coord_path); if (!isShiftDown(e) && selectedIndex === -1) { state.selectedCoordPaths = [about.coord_path]; } else if (isShiftDown(e) && selectedIndex === -1) { state.selectedCoordPaths.push(about.coord_path); } const selectedCoordinates = this.pathsToCoordinates(state.featureId, state.selectedCoordPaths); this.setSelectedCoordinates(selectedCoordinates); }; DirectSelect.onMidpoint = function(state, e) { this.startDragging(state, e); const about = e.featureTarget.properties; state.feature.addCoordinate(about.coord_path, about.lng, about.lat); this.fireUpdate(); state.selectedCoordPaths = [about.coord_path]; }; DirectSelect.pathsToCoordinates = function(featureId, paths) { return paths.map(coord_path => ({ feature_id: featureId, coord_path })); }; DirectSelect.onFeature = function(state, e) { if (state.selectedCoordPaths.length === 0) this.startDragging(state, e); else this.stopDragging(state); }; DirectSelect.dragFeature = function(state, e, delta) { moveFeatures(this.getSelected(), delta); state.dragMoveLocation = e.lngLat; }; DirectSelect.dragVertex = function(state, e, delta) { const selectedCoords = state.selectedCoordPaths.map(coord_path => state.feature.getCoordinate(coord_path)); const selectedCoordPoints = selectedCoords.map(coords => ({ type: Constants.geojsonTypes.FEATURE, properties: {}, geometry: { type: Constants.geojsonTypes.POINT, coordinates: coords } })); const constrainedDelta = constrainFeatureMovement(selectedCoordPoints, delta); for (let i = 0; i < selectedCoords.length; i++) { const coord = selectedCoords[i]; state.feature.updateCoordinate(state.selectedCoordPaths[i], coord[0] + constrainedDelta.lng, coord[1] + constrainedDelta.lat); } }; DirectSelect.clickNoTarget = function () { this.changeMode(Constants.modes.SIMPLE_SELECT); }; DirectSelect.clickInactive = function () { this.changeMode(Constants.modes.SIMPLE_SELECT); }; DirectSelect.clickActiveFeature = function (state) { state.selectedCoordPaths = []; this.clearSelectedCoordinates(); state.feature.changed(); }; // EXTERNAL FUNCTIONS DirectSelect.onSetup = function(opts) { const featureId = opts.featureId; const feature = this.getFeature(featureId); if (!feature) { throw new Error('You must provide a featureId to enter direct_select mode'); } if (feature.type === Constants.geojsonTypes.POINT) { throw new TypeError('direct_select mode doesn\'t handle point features'); } const state = { featureId, feature, dragMoveLocation: opts.startPos || null, dragMoving: false, canDragMove: false, selectedCoordPaths: opts.coordPath ? [opts.coordPath] : [], }; this.setSelectedCoordinates(this.pathsToCoordinates(featureId, state.selectedCoordPaths)); this.setSelected(featureId); doubleClickZoom.disable(this); this.setActionableState({ trash: true }); return state; }; DirectSelect.onStop = function() { doubleClickZoom.enable(this); this.clearSelectedCoordinates(); }; DirectSelect.toDisplayFeatures = function(state, geojson, push) { if (state.featureId === geojson.properties.id) { geojson.properties.active = Constants.activeStates.ACTIVE; push(geojson); createSupplementaryPoints(geojson, { map: this.map, midpoints: true, selectedPaths: state.selectedCoordPaths }).forEach(push); } else { geojson.properties.active = Constants.activeStates.INACTIVE; push(geojson); } this.fireActionable(state); }; DirectSelect.onTrash = function(state) { // Uses number-aware sorting to make sure '9' < '10'. Comparison is reversed because we want them // in reverse order so that we can remove by index safely. state.selectedCoordPaths .sort((a, b) => b.localeCompare(a, 'en', { numeric: true })) .forEach(id => state.feature.removeCoordinate(id)); this.fireUpdate(); state.selectedCoordPaths = []; this.clearSelectedCoordinates(); this.fireActionable(state); if (state.feature.isValid() === false) { this.deleteFeature([state.featureId]); this.changeMode(Constants.modes.SIMPLE_SELECT, {}); } }; DirectSelect.onMouseMove = function(state, e) { // On mousemove that is not a drag, stop vertex movement. const isFeature = isActiveFeature(e); const onVertex = isVertex(e); const isMidPoint = isMidpoint(e); const noCoords = state.selectedCoordPaths.length === 0; if (isFeature && noCoords) this.updateUIClasses({ mouse: Constants.cursors.MOVE }); else if (onVertex && !noCoords) this.updateUIClasses({ mouse: Constants.cursors.MOVE }); else this.updateUIClasses({ mouse: Constants.cursors.NONE }); const isDraggableItem = onVertex || isFeature || isMidPoint; if (isDraggableItem && state.dragMoving) this.fireUpdate(); this.stopDragging(state); // Skip render return true; }; DirectSelect.onMouseOut = function(state) { // As soon as you mouse leaves the canvas, update the feature if (state.dragMoving) this.fireUpdate(); // Skip render return true; }; DirectSelect.onTouchStart = DirectSelect.onMouseDown = function(state, e) { if (isVertex(e)) return this.onVertex(state, e); if (isActiveFeature(e)) return this.onFeature(state, e); if (isMidpoint(e)) return this.onMidpoint(state, e); }; DirectSelect.onDrag = function(state, e) { if (state.canDragMove !== true) return; state.dragMoving = true; e.originalEvent.stopPropagation(); const delta = { lng: e.lngLat.lng - state.dragMoveLocation.lng, lat: e.lngLat.lat - state.dragMoveLocation.lat }; if (state.selectedCoordPaths.length > 0) this.dragVertex(state, e, delta); else this.dragFeature(state, e, delta); state.dragMoveLocation = e.lngLat; }; DirectSelect.onClick = function(state, e) { if (noTarget(e)) return this.clickNoTarget(state, e); if (isActiveFeature(e)) return this.clickActiveFeature(state, e); if (isInactiveFeature(e)) return this.clickInactive(state, e); this.stopDragging(state); }; DirectSelect.onTap = function(state, e) { if (noTarget(e)) return this.clickNoTarget(state, e); if (isActiveFeature(e)) return this.clickActiveFeature(state, e); if (isInactiveFeature(e)) return this.clickInactive(state, e); }; DirectSelect.onTouchEnd = DirectSelect.onMouseUp = function(state) { if (state.dragMoving) { this.fireUpdate(); } this.stopDragging(state); }; export default DirectSelect;