diff --git a/dist/icons/at.svg b/dist/icons/agencies/at.svg similarity index 100% rename from dist/icons/at.svg rename to dist/icons/agencies/at.svg diff --git a/dist/icons/metlink.svg b/dist/icons/agencies/metlink.svg similarity index 100% rename from dist/icons/metlink.svg rename to dist/icons/agencies/metlink.svg diff --git a/dist/icons/normal/metro-canterbury.png b/dist/icons/agencies/metro-canterbury.png similarity index 100% rename from dist/icons/normal/metro-canterbury.png rename to dist/icons/agencies/metro-canterbury.png diff --git a/dist/icons/atlogo.png b/dist/icons/atlogo.png deleted file mode 100644 index 0b79723e..00000000 Binary files a/dist/icons/atlogo.png and /dev/null differ diff --git a/dist/icons/locate-2-fill.svg b/dist/icons/locate-2-fill.svg new file mode 100644 index 00000000..7880b15a --- /dev/null +++ b/dist/icons/locate-2-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/dist/icons/normal/bus.png b/dist/icons/normal/bus.png deleted file mode 100644 index 2a787dd5..00000000 Binary files a/dist/icons/normal/bus.png and /dev/null differ diff --git a/dist/icons/normal/bus.svg b/dist/icons/normal/bus.svg index 28906872..c71ef99e 100644 --- a/dist/icons/normal/bus.svg +++ b/dist/icons/normal/bus.svg @@ -1,36 +1 @@ - -bus 2 -Created using Figma - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/dist/icons/normal/cablecar-fill.svg b/dist/icons/normal/cablecar-fill.svg deleted file mode 100644 index eebd9668..00000000 --- a/dist/icons/normal/cablecar-fill.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/dist/icons/normal/cablecar.png b/dist/icons/normal/cablecar.png deleted file mode 100644 index fc03e972..00000000 Binary files a/dist/icons/normal/cablecar.png and /dev/null differ diff --git a/dist/icons/normal/cablecar.svg b/dist/icons/normal/cablecar.svg index cc1ee383..15259c5b 100644 --- a/dist/icons/normal/cablecar.svg +++ b/dist/icons/normal/cablecar.svg @@ -1,26 +1 @@ - -cablecar -Created using Figma - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/dist/icons/normal/default.svg b/dist/icons/normal/default.svg index 72b1dc6b..b9d80779 100644 --- a/dist/icons/normal/default.svg +++ b/dist/icons/normal/default.svg @@ -1,28 +1 @@ - -bus 2 -Created using Figma - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/dist/icons/normal/ferry-fill.svg b/dist/icons/normal/ferry-fill.svg deleted file mode 100644 index d4cebab3..00000000 --- a/dist/icons/normal/ferry-fill.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dist/icons/normal/ferry.png b/dist/icons/normal/ferry.png deleted file mode 100644 index c2da57f7..00000000 Binary files a/dist/icons/normal/ferry.png and /dev/null differ diff --git a/dist/icons/normal/ferry.svg b/dist/icons/normal/ferry.svg index 10c3d853..cfbde0be 100644 --- a/dist/icons/normal/ferry.svg +++ b/dist/icons/normal/ferry.svg @@ -1,26 +1 @@ - -ferry -Created using Figma - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/dist/icons/normal/funicular-fill.svg b/dist/icons/normal/funicular-fill.svg deleted file mode 100644 index 7aeb0c96..00000000 --- a/dist/icons/normal/funicular-fill.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dist/icons/normal/funicular.svg b/dist/icons/normal/funicular.svg deleted file mode 100644 index 7aeb0c96..00000000 --- a/dist/icons/normal/funicular.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dist/icons/normal/parkingbuilding-fill.svg b/dist/icons/normal/parkingbuilding-fill.svg deleted file mode 100644 index fe23911c..00000000 --- a/dist/icons/normal/parkingbuilding-fill.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dist/icons/normal/parkingbuilding.svg b/dist/icons/normal/parkingbuilding.svg index 5793d74b..2f5bbd93 100644 --- a/dist/icons/normal/parkingbuilding.svg +++ b/dist/icons/normal/parkingbuilding.svg @@ -1,4 +1,4 @@ - - - - \ No newline at end of file + + + + diff --git a/dist/icons/normal/telecabin-fill.svg b/dist/icons/normal/telecabin-fill.svg deleted file mode 100644 index 120ac688..00000000 --- a/dist/icons/normal/telecabin-fill.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dist/icons/normal/telecabin.svg b/dist/icons/normal/telecabin.svg deleted file mode 100644 index 120ac688..00000000 --- a/dist/icons/normal/telecabin.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dist/icons/normal/train-fill.svg b/dist/icons/normal/train-fill.svg deleted file mode 100644 index 9805838c..00000000 --- a/dist/icons/normal/train-fill.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dist/icons/normal/train.png b/dist/icons/normal/train.png deleted file mode 100644 index 4e61f513..00000000 Binary files a/dist/icons/normal/train.png and /dev/null differ diff --git a/dist/icons/normal/train.svg b/dist/icons/normal/train.svg index 2f97b241..2b559b8c 100644 --- a/dist/icons/normal/train.svg +++ b/dist/icons/normal/train.svg @@ -1,26 +1 @@ - -train -Created using Figma - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/dist/icons/normal/vehicle-bike.png b/dist/icons/normal/vehicle-bike.png new file mode 100644 index 00000000..03e91ad2 Binary files /dev/null and b/dist/icons/normal/vehicle-bike.png differ diff --git a/dist/icons/normal/vehicle-bus.png b/dist/icons/normal/vehicle-bus.png new file mode 100644 index 00000000..474d6dab Binary files /dev/null and b/dist/icons/normal/vehicle-bus.png differ diff --git a/dist/icons/normal/vehicle-cablecar.png b/dist/icons/normal/vehicle-cablecar.png new file mode 100644 index 00000000..183a9016 Binary files /dev/null and b/dist/icons/normal/vehicle-cablecar.png differ diff --git a/dist/icons/normal/vehicle-ferry.png b/dist/icons/normal/vehicle-ferry.png new file mode 100644 index 00000000..902716a6 Binary files /dev/null and b/dist/icons/normal/vehicle-ferry.png differ diff --git a/dist/icons/normal/vehicle-parkingbuilding.png b/dist/icons/normal/vehicle-parkingbuilding.png new file mode 100644 index 00000000..e02df293 Binary files /dev/null and b/dist/icons/normal/vehicle-parkingbuilding.png differ diff --git a/dist/icons/normal/vehicle-train.png b/dist/icons/normal/vehicle-train.png new file mode 100644 index 00000000..e8e161a0 Binary files /dev/null and b/dist/icons/normal/vehicle-train.png differ diff --git a/dist/icons/parkingbuilding.svg b/dist/icons/parkingbuilding.svg new file mode 100644 index 00000000..2f5bbd93 --- /dev/null +++ b/dist/icons/parkingbuilding.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dist/icons/telecabin-fill.svg b/dist/icons/telecabin-fill.svg deleted file mode 100644 index 120ac688..00000000 --- a/dist/icons/telecabin-fill.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dist/icons/telecabin.svg b/dist/icons/telecabin.svg deleted file mode 100644 index 120ac688..00000000 --- a/dist/icons/telecabin.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/js/data/StationData.js b/js/data/StationData.js index 6f9d9bdd..ac2e4ef5 100644 --- a/js/data/StationData.js +++ b/js/data/StationData.js @@ -1,4 +1,5 @@ import { endpoint } from '../../local' +import StationStore from '../stores/StationStore.js' import { t } from '../stores/translationStore.js' class StationData { @@ -27,6 +28,9 @@ class StationData { } const data = await res.json() + // gross, shouldn't be here... + StationStore.stationCache[stopCode] = data + const stopId = data.stop_id let name = data.stop_name const routeType = data.route_type diff --git a/js/helpers/icons/au-syd.js b/js/helpers/icons/au-syd.js deleted file mode 100644 index ba17a9cf..00000000 --- a/js/helpers/icons/au-syd.js +++ /dev/null @@ -1,20 +0,0 @@ -export const auSydMetadata = { - default: { - size: { - width: 25, - height: 25, - }, - }, - 'default-selection': { - size: { - width: 25, - height: 25, - }, - }, - train: {}, - bus: {}, - ferry: {}, - coach: {}, - lightrail: {}, - subway: {}, -} diff --git a/js/helpers/icons/index.js b/js/helpers/icons/index.js deleted file mode 100644 index 5cb0496c..00000000 --- a/js/helpers/icons/index.js +++ /dev/null @@ -1,113 +0,0 @@ -import { icon as Icon } from 'leaflet' -import { normalMetadata } from './normal.js' -import { auSydMetadata } from './au-syd.js' - -// this map is from the routeType enum to the filename -// e.g if a stop had the route type 4, it would look for ferry.svg -const routeTypeMap = new Map() -routeTypeMap.set(0, 'lightrail') -routeTypeMap.set(1, 'subway') -routeTypeMap.set(2, 'train') -routeTypeMap.set(3, 'bus') -routeTypeMap.set(4, 'ferry') -routeTypeMap.set(5, 'cablecar') -routeTypeMap.set(6, 'gondola') -routeTypeMap.set(7, 'funicular') -routeTypeMap.set(-1, 'parkingbuilding') -routeTypeMap.set(-2, 'bike') -routeTypeMap.set(200, 'coach') -routeTypeMap.set(400, 'train') -routeTypeMap.set(401, 'subway') -routeTypeMap.set(900, 'lightrail') - -const iconStyles = { - normal: normalMetadata, - 'au-syd': auSydMetadata, -} - -class Iconhelper { - getClassName(prefix, variant) { - if (variant !== null && prefix === 'normal') { - return 'currentSelectionIcon larger' - } - return '' - } - - getFileName(prefix, routeType, variant) { - const pathPrefix = `/icons/${prefix}/` - const fileType = '.svg' - - // legacy used fill instead of selection - if (prefix === 'normal' && variant === 'selection') { - return `${pathPrefix}${routeType}-fill${fileType}` - } - return `${pathPrefix}${routeType}${variant ? `-${variant}` : ''}${fileType}` - } - - getIcon(prefixRequest, routeTypeEnum, variant = null) { - const icon = {} - - let prefix = 'normal' - let routeType = this.getRouteType(routeTypeEnum) - // use the icon if it is defined - if ( - iconStyles[prefixRequest] !== undefined && - iconStyles[prefixRequest][routeType] !== undefined - ) { - prefix = prefixRequest - } - // fallback to the normal icons, if one exists - if (iconStyles[prefix][routeType] === undefined) { - routeType = 'default' - } - - icon.iconUrl = this.getFileName(prefix, routeType, variant) - icon.iconSize = this.getSize(prefix, routeTypeEnum, variant) - icon.className = this.getClassName(prefix, variant) - return new Icon(icon) - } - - getSize(prefixRequest, routeTypeEnum, variant = null) { - // handles the differences between routeTypes - let routeType = this.getRouteType(routeTypeEnum) - let defaultIcon = 'default' - if (variant !== null) { - routeType += '-selection' - defaultIcon += '-selection' - } - - // returns the size of the icon, or uses the default size if not defined - const prefix = - iconStyles[prefixRequest] === undefined ? 'normal' : prefixRequest - let { size } = iconStyles[prefix][defaultIcon] - if ( - iconStyles[prefix][routeType] !== undefined && - iconStyles[prefix][routeType].size !== undefined - ) { - ;({ size } = iconStyles[prefix][routeType]) - } - return [size.width, size.height] - } - - getRouteType(routeTypeEnum) { - let routeTypeQuery = routeTypeEnum - // all trains are trains - if (routeTypeEnum >= 100 && routeTypeEnum <= 199) { - routeTypeQuery = 2 - } - // all coaches are coaches - if (routeTypeEnum >= 200 && routeTypeEnum <= 299) { - routeTypeQuery = 200 - } - // all buses are buses - if (routeTypeEnum >= 700 && routeTypeEnum <= 799) { - routeTypeQuery = 3 - } - if (routeTypeMap.get(routeTypeQuery) === undefined) { - return 'default' - } - return routeTypeMap.get(routeTypeQuery) - } -} - -export default Iconhelper diff --git a/js/helpers/icons/normal.js b/js/helpers/icons/normal.js deleted file mode 100644 index 4ed75c63..00000000 --- a/js/helpers/icons/normal.js +++ /dev/null @@ -1,34 +0,0 @@ -// How this file works! -// -// If there is no matching override, the application will fallback -// to the value specified in default. -// Keys will have to exist though, otherwise the unknown icon is used. -export const normalMetadata = { - default: { - size: { - width: 28, - height: 34, - }, - }, - 'default-selection': { - size: { - width: 28, - height: 28, - }, - }, - train: {}, - bus: { - size: { - width: 26, - height: 32, - }, - }, - ferry: {}, - cablecar: {}, - parkingbuilding: { - size: { - width: 28, - height: 28, - }, - }, -} diff --git a/js/stores/CurrentLocation.js b/js/stores/CurrentLocation.js index 339f0d14..3641f6a8 100644 --- a/js/stores/CurrentLocation.js +++ b/js/stores/CurrentLocation.js @@ -1,71 +1,16 @@ -import { vars } from '../styles.js' import Events from './Events' import StationStore from './StationStore' -import SettingsStore from './SettingsStore' - -const { desktopThreshold } = vars class CurrentLocation extends Events { // ability to subscribe to location updates constructor(props) { super(props) - this.geoID = null // geoID has a watcg on the watching of position this.state = { position: [0, 0], // user's current position - gpsPosition: [0, 0], // same same but different? - accuracy: 0, // accuracy in (km?) of user's current position - timestamp: 0, // what time location was found - error: '', // if geolocation returned error - hasGranted: false, initialSet: window.location.pathname.split('/')[1] !== 's', } } - async componentDidMount() { - // this only works in chrome & firefox not safari whoops. - // also, don't do it on desktop - if ('permissions' in navigator && window.innerWidth <= desktopThreshold) { - const e = await navigator.permissions.query({ name: 'geolocation' }) - if (e.state === 'granted') { - this.state.hasGranted = true - } - } - } - - startWatch(updateType = 'pinmove') { - // use this on page load to start watching poistion - this.geoID = navigator.geolocation.watchPosition( - position => { - // make the geoID watch the position - this.state.hasGranted = true - this.setCurrentPosition(position, updateType) - - // can only move the map once - if (updateType === 'mapmove') { - updateType = 'pinmove' - } - - // stops watching geolocation after 20 seconds - setTimeout(() => { - this.stopWatch() - }, 20000) - }, - error => { - this.state.error = error.message - }, - { - enableHighAccuracy: true, - timeout: 5000, - } - ) - } - - stopWatch() { - // releases watch on geoID - navigator.geolocation.clearWatch(this.geoID) - this.geoID = null - } - setInitialPosition(lat, lng) { if (this.state.initialSet === false) { this.state.position = [lat, lng] @@ -74,48 +19,10 @@ class CurrentLocation extends Events { } } - setCurrentPosition(position, updateType = 'pinmove') { - const coords = [position.coords.latitude, position.coords.longitude] - this.state.position = coords - this.state.gpsPosition = coords - this.state.accuracy = position.coords.accuracy - this.state.timestamp = position.timestamp - this.trigger(updateType) - requestAnimationFrame(() => { - SettingsStore.state.lastLocation = coords - SettingsStore.saveState() - }) - } - - resetCurrentPosition(updateType = 'pinmove') { - this.state.position[0] = this.state.gpsPosition[0] + Math.random() / 100000 - this.state.position[1] = this.state.gpsPosition[1] + Math.random() / 100000 - this.trigger(updateType) - } - setCity(prefix, position) { this.state.position = position this.trigger('mapmove-silent') StationStore.getCity(...this.state.position) } - - currentLocationButton() { - if (this.geoID === null) { - this.startWatch('mapmove') - return - } - if (this.state.error === '') { - // if no error - this.resetCurrentPosition('mapmove') // move position by a very small random amount - } else if (this.state.error.toLowerCase() === 'timeout expired') { - this.resetCurrentPosition('mapmove') - navigator.geolocation.getCurrentPosition(position => { - this.state.hasGranted = true - this.setCurrentPosition(position, 'mapmove') - }) - } else { - alert(this.state.error) - } - } } export default new CurrentLocation() diff --git a/js/stores/SettingsStore.js b/js/stores/SettingsStore.js index 48864ecc..0ab00e8c 100644 --- a/js/stores/SettingsStore.js +++ b/js/stores/SettingsStore.js @@ -12,7 +12,7 @@ class SettingsStore { this.state[attr] = preState[attr] }) } - localStorage.setItem('AppVersion', '2.4.6') + localStorage.setItem('AppVersion', '3.0.0') } getState() { diff --git a/js/stores/StationStore.js b/js/stores/StationStore.js index f6564914..d4bd26e6 100644 --- a/js/stores/StationStore.js +++ b/js/stores/StationStore.js @@ -2,13 +2,11 @@ import Events from './Events' import local from '../../local.js' import SettingsStore from './SettingsStore.js' import { t } from './translationStore.js' -import IconHelper from '../helpers/icons/index.js' +import { getIconName } from '../views/maps/util.jsx' class StationStore extends Events { stationCache = {} - iconHelper = new IconHelper() - constructor(props) { super(props) this.currentCity = { @@ -131,7 +129,7 @@ class StationStore extends Events { const dataCollection = await Promise.all(promises) dataCollection.forEach((data, key) => { const no = stopNumber.split('+')[key] - const icon = this.iconHelper.getRouteType(data.route_type) + const icon = getIconName(region, data.route_type, 'SavedStations') let description = t('savedStations.stop', { number: `${no} / ${data.stop_name}`, }) diff --git a/js/stores/UiStore.js b/js/stores/UiStore.js index e2b80025..b01a7134 100644 --- a/js/stores/UiStore.js +++ b/js/stores/UiStore.js @@ -15,7 +15,7 @@ class UIStore extends Events { // base map stuff basemap: null, - basemapType: 'leaflet', + basemapType: 'mapbox', // root card positions oldCardPosition: 'default', diff --git a/js/views/lines/AllLines.jsx b/js/views/lines/AllLines.jsx deleted file mode 100644 index ce148daa..00000000 --- a/js/views/lines/AllLines.jsx +++ /dev/null @@ -1,377 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { View, Text, StyleSheet } from 'react-native' -import leaflet from 'leaflet' -import { withRouter } from 'react-router' - -import local from '../../../local.js' -import { vars } from '../../styles.js' -import UiStore from '../../stores/UiStore.js' -import Header from '../reusable/Header.jsx' -import LinkedScroll from '../reusable/LinkedScroll.jsx' - -import Layer from '../maps/Layer.jsx' - -const Icon = leaflet.icon -const icons = new Map([ - [ - 2, - Icon({ - iconUrl: '/icons/normal/train-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 3, - Icon({ - iconUrl: '/icons/normal/bus-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 700, - Icon({ - iconUrl: '/icons/normal/bus-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 900, - Icon({ - iconUrl: '/icons/normal/cablecar-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 4, - Icon({ - iconUrl: '/icons/normal/ferry-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 100, - Icon({ - iconUrl: '/icons/normal/train-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 106, - Icon({ - iconUrl: '/icons/normal/train-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 400, - Icon({ - iconUrl: '/icons/normal/train-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 401, - Icon({ - iconUrl: '/icons/normal/train-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], -]) - -let styles = null - -class AllLines extends React.Component { - static propTypes = { - match: PropTypes.object.isRequired, - } - - layer = new Layer() - - pointsLayer = new Layer() - - liveLayer = new Layer() - - state = { - color: '#666', - trains: false, - buses: false, - lightrail: false, - ferries: false, - } - - constructor(props) { - super(props) - - if ( - UiStore.state.lastTransition !== 'backward' && - UiStore.state.cardPosition === 'max' - ) { - requestAnimationFrame(() => { - UiStore.setCardPosition('default') - }) - } - } - - componentDidMount() { - this.getPositionData() - - this.liveRefresh = setInterval(() => { - this.getPositionData() - }, 10000) - } - - componentWillUnmount() { - this.layer.hide(true, true) - this.pointsLayer.hide() - this.liveLayer.hide() - this.layer.unmounted = true - this.pointsLayer.unmounted = true - this.liveLayer.unmounted = true - - clearInterval(this.liveRefresh) - this.cancelCallbacks = true - } - - async getRealtime() { - const { buses, ferries, trains, lightrail } = this.state - console.log(this.state) - const res = await fetch( - `${local.endpoint}/${this.props.match.params.region}/realtime/all?ferries=${ferries}&buses=${buses}&trains=${trains}&lightrail=${lightrail}` - ) - - const data = await res.json() - if (res.status >= 400) { - const error = new Error(data.message) - error.response = data - throw error - } - return data - } - - getPositionData = async () => { - console.log(this.state) - let busPositions = null - let trainPositions = null - let lightRailPositions = null - let ferryPostitions = null - try { - const data = await this.getRealtime() - this.liveLayer.hide() - this.liveLayer = new Layer() - busPositions = { - type: 'MultiPoint', - coordinates: [], - } - trainPositions = { - type: 'MultiPoint', - coordinates: [], - } - lightRailPositions = { - type: 'MultiPoint', - coordinates: [], - } - ferryPostitions = { - type: 'MultiPoint', - coordinates: [], - } - data.forEach(trip => { - if (trip.latitude !== undefined) { - switch (trip.route_type) { - case 3: - case 700: - case 712: - busPositions.coordinates.push([trip.longitude, trip.latitude]) - - break - case 2: - case 400: - case 401: - case 100: - case 106: - trainPositions.coordinates.push([trip.longitude, trip.latitude]) - break - case 0: - case 900: - lightRailPositions.coordinates.push([ - trip.longitude, - trip.latitude, - ]) - break - case 4: - case 1000: - ferryPostitions.coordinates.push([trip.longitude, trip.latitude]) - break - default: - break - } - } - }) - const busIcon = icons.get(3) - const trainIcon = icons.get(2) - const lightRailIcon = icons.get(900) - const ferryIcon = icons.get(4) - const { buses, ferries, trains, lightrail } = this.state - if (buses) { - this.liveLayer.add('geojson', busPositions, { - icon: busIcon, - }) - } - if (trains) { - this.liveLayer.add('geojson', trainPositions, { - icon: trainIcon, - }) - } - if (lightrail) { - this.liveLayer.add('geojson', lightRailPositions, { - icon: lightRailIcon, - }) - } - if (ferries) { - this.liveLayer.add('geojson', ferryPostitions, { - icon: ferryIcon, - }) - } - - if (this.cancelCallbacks === true) return 'cancelled' - this.liveLayer.show() - return 'done' - } catch (err) { - console.log(err) - // who cares about the error - console.error('Could not load realtime.') - } - } - - trainsOn = () => { - this.setState(state => ({ trains: !state.trains })) - this.getPositionData() - } - - ferriesOn = () => { - this.setState(state => ({ ferries: !state.ferries })) - this.getPositionData() - } - - lightrailOn = () => { - this.setState(state => ({ lightrail: !state.lightrail })) - this.getPositionData() - } - - busesOn = () => { - this.setState(state => ({ buses: !state.buses })) - this.getPositionData() - } - - render() { - const { match } = this.props - const { - error, - errorMessage, - loading, - trains, - ferries, - lightrail, - buses, - } = this.state - - if (error) { - return ( - -
- - - We couldn't load the {match.params.route_short_name} line in{' '} - {match.params.region}. - - {errorMessage} - - - ) - } - - const inner = loading ? ( -
- ) : ( - - - - - - - ) - - return ( - -
- {inner} - - ) - } -} - -styles = StyleSheet.create({ - wrapper: { - flex: 1, - }, - direction: { - paddingTop: vars.padding, - paddingLeft: vars.padding, - paddingBottom: vars.padding * 0.5, - fontWeight: '600', - fontSize: vars.defaultFontSize, - fontFamily: vars.defaultFontFamily, - }, - linkWrapper: { - padding: vars.padding, - }, - error: { - padding: vars.padding, - }, - errorMessage: { - fontSize: vars.defaultFontSize, - fontFamily: vars.defaultFontFamily, - }, -}) -export default withRouter(AllLines) diff --git a/js/views/lines/Line.jsx b/js/views/lines/Line.jsx index 9d7ee402..a88f38b0 100644 --- a/js/views/lines/Line.jsx +++ b/js/views/lines/Line.jsx @@ -1,7 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' import { View, StyleSheet } from 'react-native' -import leaflet from 'leaflet' import { withRouter } from 'react-router' import queryString from 'query-string' @@ -10,61 +9,15 @@ import UiStore from '../../stores/UiStore.js' import Header from '../reusable/Header.jsx' import LinkedScroll from '../reusable/LinkedScroll.jsx' -import Layer from '../maps/Layer.jsx' +import Layer from '../maps/MapboxLayer.jsx' import LineData from '../../data/LineData.js' import { LineStops } from './stops/LineStops.jsx' import { LineTimetable } from './timetable/LineTimetable.jsx' import { LineErrorBoundary } from './LineErrorBoundary.jsx' -import IconHelper from '../../helpers/icons/index.js' import TripInfo from './TripInfo.jsx' import LineIcon from '../../../dist/icons/linepicker.svg' -const Icon = leaflet.icon -const icons = new Map([ - [ - 'train', - Icon({ - iconUrl: '/icons/normal/train-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 'subway', - Icon({ - iconUrl: '/icons/normal/train-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 'bus', - Icon({ - iconUrl: '/icons/normal/bus-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 'cablecar', - Icon({ - iconUrl: '/icons/normal/cablecar-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], - [ - 4, - 'ferry', - Icon({ - iconUrl: '/icons/normal/ferry-fill.svg', - iconSize: [24, 24], - className: 'vehIcon', - }), - ], -]) - let styles = null class Line extends React.Component { @@ -72,9 +25,7 @@ class Line extends React.Component { match: PropTypes.object.isRequired, } - liveLayer = new Layer() - - iconHelper = new IconHelper() + liveLayer = new Layer('live-vehicles') tripStops = [] @@ -122,7 +73,7 @@ class Line extends React.Component { } componentWillUnmount() { - this.liveLayer.hide() + this.liveLayer.hide(true, false) this.liveLayer.unmounted = true clearInterval(this.liveRefresh) @@ -160,8 +111,8 @@ class Line extends React.Component { let busPositions = null try { const data = await this.lineData.getRealtime() - this.liveLayer.hide() - this.liveLayer = new Layer() + this.liveLayer.hide(true, true) + this.liveLayer = new Layer('live-vehicles') busPositions = { type: 'MultiPoint', coordinates: [], @@ -183,11 +134,13 @@ class Line extends React.Component { await this.dataResolved const { lineMetadata } = this.state if (lineMetadata.length === 0) return 'cancelled' // this if it the line can't loa - const icon = icons.get( - this.iconHelper.getRouteType(lineMetadata[0].route_type) - ) this.liveLayer.add('geojson', busPositions, { - icon, + orderBefore: 'route-points-popups', + typeExtension: 'VehicleMarker', + typeExtensionOptions: { + region: this.lineData.region, + route_type: lineMetadata[0].route_type, + }, }) if (this.cancelCallbacks === true) return 'cancelled' this.liveLayer.show() diff --git a/js/views/lines/lineCommon.jsx b/js/views/lines/lineCommon.jsx index 7410ce9d..44ae4dce 100644 --- a/js/views/lines/lineCommon.jsx +++ b/js/views/lines/lineCommon.jsx @@ -1,11 +1,13 @@ +import mapboxgl from 'mapbox-gl' + import { t } from '../../stores/translationStore.js' import UiStore from '../../stores/UiStore.js' export const renderShape = (shape, layer, routeColor) => { layer.add('geojson', shape, { + orderBefore: 'route-points', color: routeColor, className: 'metro-line', - order: 'back', }) layer.show(shape.bounds, true, false) return shape @@ -19,57 +21,52 @@ export const renderStops = ( routeShortName ) => { const geojson = { - type: 'MultiPoint', - coordinates: [], + type: 'FeatureCollection', + features: stops.map(stop => ({ + type: 'Feature', + properties: { + id: stop.stop_id, + name: stop.stop_name, + }, + geometry: { + type: 'Point', + coordinates: [stop.stop_lon, stop.stop_lat], + }, + })), } - const stopsMap = {} - stops.forEach(stop => { - geojson.coordinates.push([stop.stop_lon, stop.stop_lat]) - stopsMap[[stop.stop_lat, stop.stop_lon].join(',')] = stop - }) pointsLayer.add('geojson', geojson, { + orderBefore: 'live-vehicles', typeExtension: 'CircleMarker', typeExtensionOptions: { - className: 'metro-dot', color: routeColor, - radius: 4, - }, - maxZoom: 5, - }) - pointsLayer.add('geojson', geojson, { - typeExtension: 'InvisibleMarker', - typeExtensionOptions: { - zIndexOffset: 30, - popupContent: (lat, lng) => { - const data = stopsMap[[lat, lng].join(',')] - return ( - // it's not quite react + radius: 3, + popupContent: e => { + const coordinates = e.features[0].geometry.coordinates.slice() + const name = e.features[0].properties.name + const id = e.features[0].properties.id + + const popup = new mapboxgl.Popup({ + closeButton: false, + className: 'mapbox-stops-popup', + }) + .setLngLat(coordinates) + .setHTML( + ` +

${name}

+

${t('vech_loc.stop', { number: id })}

+ ` - -

${data.stop_name}

-

${t('vech_loc.stop', { number: data.stop_id })}

- -
` - ) - }, - popupOpen: e => { - const elem = e.popup.getElement() - const { station } = elem.querySelector('[data-station]').dataset - const baseUrl = `/s/${region}/${station}` - const extendedUrl = `${baseUrl}/timetable/${routeShortName}-2` + ) - elem - .querySelector('.leaflet-service-button') - .addEventListener('click', () => { - UiStore.safePush(baseUrl) - }) - elem - .querySelector('.leaflet-timetable-button') - .addEventListener('click', () => { - UiStore.safePush(extendedUrl) - }) + popup.on('open', e => { + document + .querySelector('.mapbox-stops-popup button') + .addEventListener('click', () => { + popup.remove() + UiStore.safePush(`/s/${region}/${id}`) + }) + }) + popup.addTo(UiStore.state.basemap) }, }, }) diff --git a/js/views/lines/stops/LineStops.jsx b/js/views/lines/stops/LineStops.jsx index 561acbb8..fd598b0d 100644 --- a/js/views/lines/stops/LineStops.jsx +++ b/js/views/lines/stops/LineStops.jsx @@ -3,7 +3,7 @@ import { View, Text, StyleSheet } from 'react-native' import { vars } from '../../../styles.js' import SettingsStore from '../../../stores/SettingsStore.js' -import Layer from '../../maps/Layer.jsx' +import Layer from '../../maps/MapboxLayer.jsx' import Spinner from '../../reusable/Spinner.jsx' import { renderShape, renderStops } from '../lineCommon.jsx' import LineData from '../../../data/LineData.js' @@ -17,7 +17,7 @@ export class LineStops extends Component { const { region } = this.props this.lineData = new LineData({ region }) - this.pointsLayer = new Layer() + this.pointsLayer = new Layer('route-points') this.shapesLayer = null this.interpolatedShape = null this.isShapeLoaded = false @@ -25,9 +25,12 @@ export class LineStops extends Component { this.state = { loading: true, } + + this.mounted = false } componentDidMount() { + this.mounted = true this.getStops() } @@ -40,14 +43,13 @@ export class LineStops extends Component { } componentWillUnmount() { + this.mounted = false const { pointsLayer, shapesLayer } = this // shapeslayer is nullable if (shapesLayer) { shapesLayer.hide(true, true) - shapesLayer.unmounted = true } pointsLayer.hide() - pointsLayer.unmounted = true } getStops = async () => { @@ -57,6 +59,7 @@ export class LineStops extends Component { this.lineData.trip_id = tripId const { current, next, routeInfo } = await this.lineData.getTripStops() + if (!this.mounted) return this.setState({ current, next, @@ -64,6 +67,7 @@ export class LineStops extends Component { loading: false, }) + pointsLayer.hide() renderStops( current, pointsLayer, @@ -91,6 +95,7 @@ export class LineStops extends Component { this.shapesLayer.hide(true, true) this.shapesLayer = new Layer() } + if (!this.mounted) return return renderShape(shape, this.shapesLayer, color) }) .catch(() => { @@ -106,6 +111,7 @@ export class LineStops extends Component { type: 'LineString', coordinates: stops.map(stop => [stop.stop_lon, stop.stop_lat]), } + if (!this.mounted) return this.interpolatedShape = renderShape( { ...shape, ...this.lineData.getShapeBounds(shape) }, shapesLayer, diff --git a/js/views/maps/BaseMap.jsx b/js/views/maps/BaseMap.jsx deleted file mode 100644 index ac08f318..00000000 --- a/js/views/maps/BaseMap.jsx +++ /dev/null @@ -1,504 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { StyleSheet, TouchableOpacity } from 'react-native' -import leaflet from 'leaflet' -import * as reactLeaflet from 'react-leaflet' - -import { withRouter } from 'react-router' - -import local from '../../../local' - -import { vars } from '../../styles.js' -import CurrentLocation from '../../stores/CurrentLocation.js' -import StationStore from '../../stores/StationStore.js' -import SettingsStore from '../../stores/SettingsStore.js' -import UiStore from '../../stores/UiStore.js' -import { t } from '../../stores/translationStore.js' - -import LocateIcon from '../../../dist/icons/locate-2.svg' - -import IconHelper from '../../helpers/icons/index.js' - -const Icon = leaflet.icon -const { CRS, point, latLng } = leaflet -const LeafletMap = reactLeaflet.Map -const { Marker, TileLayer, ZoomControl, Circle, CircleMarker } = reactLeaflet - -const getDist = function(zoom) { - let screensize = document.body.offsetWidth - if (document.body.offsetHeight > screensize) { - screensize = document.body.offsetHeight - } - let dist = Math.ceil(0.2 * screensize) - if (zoom === 17) { - dist = Math.ceil(0.45 * screensize) - } else if (zoom === 16) { - dist = Math.ceil(0.8 * screensize) - } - // max the api will handle is 1250 - if (dist > 1250) { - dist = 1250 - } - return dist -} - -const getMarker = function(iconType, name) { - if (iconType === 'bus') { - name = name - .trim() - .replace(/\)/g, '') - .replace(/\(/g, '') - if (name.substring(3, 4) === ' ' || name.length === 3) { - name = name.substring(0, 3) - } else if (name.substring(2, 3) === ' ' || name.length === 2) { - name = name.substring(0, 2) - } else { - name = name.substring(0, 1) - } - name = name.replace(/ /g, '').toUpperCase() - const dynamic = ` - - - - - - - - - - - - - - - - - ${name} - - - - - - ` - return Icon({ - iconUrl: `data:image/svg+xml;charset=utf-8,${encodeURIComponent( - dynamic - )}`, - iconSize: [25, 41], - }) - } -} - -const calculateSnap = (stop, collisionMap) => { - const markerWidth = 28 - const markerHeight = 34 - - const xSnap = Math.floor(stop.x / markerWidth / 1) - const ySnap = Math.floor(stop.y / markerHeight / 1) - - // checks itself, plus the 8 positions around it - const foundColision = [ - (collisionMap[xSnap] || {})[ySnap], - (collisionMap[xSnap + 1] || {})[ySnap], - (collisionMap[xSnap + 1] || {})[ySnap + 1], - (collisionMap[xSnap] || {})[ySnap + 1], - (collisionMap[xSnap - 1] || {})[ySnap + 1], - (collisionMap[xSnap - 1] || {})[ySnap], - (collisionMap[xSnap - 1] || {})[ySnap - 1], - (collisionMap[xSnap] || {})[ySnap - 1], - (collisionMap[xSnap + 1] || {})[ySnap - 1], - ].filter(f => f !== undefined)[0] - - collisionMap[xSnap] = collisionMap[xSnap] || {} - if ( - collisionMap[xSnap][ySnap] === undefined && - collisionMap[xSnap][ySnap] === undefined - ) { - collisionMap[xSnap][ySnap] = stop - } - - if (foundColision !== undefined) { - const xDelta = foundColision.x - stop.x - const yDelta = foundColision.y - stop.y - - if (Math.abs(xDelta) > Math.abs(yDelta) || xDelta === yDelta) { - if (Math.abs(xDelta) < markerWidth) { - if (xDelta < 0) { - stop.x = stop.x + xDelta + markerWidth + 1 - } else { - stop.x = stop.x + xDelta - markerWidth - 1 - } - } - } else if (Math.abs(yDelta) < markerHeight) { - if (yDelta < 0) { - stop.y = stop.y + yDelta + markerHeight + 1 - } else { - stop.y = stop.y + yDelta - markerHeight - 1 - } - } - const nxSnap = stop.x - const nySnap = stop.y - collisionMap[stop.x] = collisionMap[nxSnap] || {} - if (collisionMap[nxSnap][nySnap] === undefined) { - collisionMap[nxSnap][nySnap] = stop - } else { - // bail out - } - } -} - -// If we stop binding this to the history, we can make this pure -class BaseMap extends React.Component { - static propTypes = { - history: PropTypes.object.isRequired, - } - - map = React.createRef() - - iconHelper = new IconHelper() - - myIcons = {} - - state = { - stops: [], - position: SettingsStore.getState().lastLocation, - positionMarker: [0, 0], - initialPosition: true, - loadmap: true, - hideStops: false, - online: window.navigator.onLine, - } - - zoom = 17 - - position = [...SettingsStore.getState().lastLocation, getDist(this.zoom)] - - componentDidMount() { - window.addEventListener('online', this.triggerRetry) - window.addEventListener('offline', this.goOffline) - CurrentLocation.bind('pinmove', this.pinmove) - CurrentLocation.bind('mapmove', this.mapmove) - CurrentLocation.bind('mapmove-silent', this.mapmovesilent) - UiStore.bind('stop-visibility', this.stopVisibility) - this.getData(this.position[0], this.position[1], this.position[2]) - - UiStore.basemap = this.map.current.leafletElement - - if (CurrentLocation.state.hasGranted) { - CurrentLocation.startWatch() - } - } - - componentWillUnmount() { - window.removeEventListener('online', this.triggerRetry) - window.removeEventListener('offline', this.goOffline) - CurrentLocation.unbind('pinmove', this.pinmove) - CurrentLocation.unbind('mapmove', this.mapmove) - CurrentLocation.unbind('mapmove-silent', this.mapmovesilent) - UiStore.unbind('stop-visibility', this.stopVisibility) - CurrentLocation.stopWatch() - } - - stopVisibility = state => { - if (this.state.hideStops !== state) { - this.setState({ - hideStops: state, - }) - if (state === false && this.zoom > 15) { - this.getData(this.position[0], this.position[1], this.position[2]) - } - } - } - - pinmove = () => { - if (this.state.initialPosition) { - this.mapmove() - } else { - this.setState({ - positionMarker: CurrentLocation.state.position.slice(), - }) - } - } - - mapmove = () => { - this.zoom = 17 - const position = CurrentLocation.state.position.slice() - position[0] += 0.00000001 - this.setState({ - position, - positionMarker: CurrentLocation.state.position.slice(), - initialPosition: false, - }) - } - - mapmovesilent = () => { - this.zoom = 17 - this.setState({ - position: CurrentLocation.state.position.slice(), - initialPosition: false, - }) - } - - async getData(lat, lon, dist) { - this.position = [lat, lon, dist] - try { - const res = await fetch( - `${local.endpoint}/auto/station/search?lat=${lat.toFixed( - 4 - )}&lon=${lon.toFixed(4)}&distance=${dist}` - ) - const data = await res.json() - data.forEach(item => { - StationStore.stationCache[item.stop_id] = item - const key = [item.stop_region, item.route_type].join(':') - if (this.myIcons[key] === undefined) { - this.myIcons[key] = this.iconHelper.getIcon( - item.stop_region, - item.route_type - ) - } - }) - this.setState({ - stops: data, - }) - } catch (error) { - console.log(error) - } - } - - triggerCurrentLocation = () => { - CurrentLocation.currentLocationButton() - } - - viewServices = (station, region) => () => { - const { history } = this.props - const split = history.location.pathname.split('/') - const currentStation = `/s/${region}/${station}` - if (split[1] === 's' && split.length === 4) { - history.replace(currentStation) - } else { - history.push(currentStation) - } - } - - moveEnd = e => { - const zoom = e.target.getZoom() - this.zoom = zoom - const newPos = e.target.getCenter() - - if ( - this.state.stops.length > 0 && - this.state.stops[0].stop_region !== StationStore.currentCity.prefix - ) { - StationStore.getCity(newPos.lat, newPos.lng) - } - let dist = 0 - if (zoom < 16 || this.state.hideStops) { - this.setState({ - stops: [], - }) - return - } - dist = getDist(zoom) - this.getData(newPos.lat, newPos.lng, dist) - } - - triggerRetry = () => { - this.setState({ - loadmap: false, - online: window.navigator.onLine, - }) - setTimeout(() => { - this.setState({ - loadmap: true, - }) - this.getData(this.position[0], this.position[1], 250) - }, 50) - } - - goOffline = () => { - this.setState({ - online: false, - }) - } - - render() { - const { stops } = this.state - const collisionMap = {} - stops.forEach(stop => { - const point = CRS.EPSG3857.latLngToPoint( - latLng(stop.stop_lat, stop.stop_lon), - this.zoom - ) - stop.x = point.x - stop.y = point.y - calculateSnap(stop, collisionMap) - }) - // changes stuff in place (a bit gross) - Object.keys(collisionMap).forEach(a => { - Object.keys(collisionMap[a]).forEach(b => { - const stop = collisionMap[a][b] - const coords = CRS.EPSG3857.pointToLatLng( - point(stop.x, stop.y), - this.zoom - ) - stop.stop_lat = coords.lat - stop.stop_lon = coords.lng - return b - }) - }) - - let stationMarker = null - const splitName = window.location.pathname.split('/') - if (splitName.length === 4 && splitName[1][0] === 's') { - const currentStation = splitName[3] - const item = StationStore.stationCache[currentStation] - if (item !== undefined) { - const icon = this.iconHelper.getRouteType(item.route_type) - const markericon = this.iconHelper.getIcon( - item.stop_region, - item.route_type, - 'selection' - ) - stationMarker = ( - - ) - } - } - - const positionMap = {} - - let bigCircle - if (this.state.accuracy < 500) { - bigCircle = ( - - ) - } - - let offline = null - if (!this.state.online) { - offline = ( -
-

You are not connected to the internet.

- -
- ) - } - - let mapview - if (this.state.loadmap) { - mapview = ( - - - OpenMapTiles | © OpenStreetMap' - } - /> - {this.state.hideStops - ? null - : this.state.stops.map(stop => { - const icon = this.iconHelper.getRouteType(stop.route_type) - let markericon = this.myIcons[ - [stop.stop_region, stop.route_type].join(':') - ] - if (icon === 'bus') { - const stopSplit = stop.stop_name.split('Stop') - const platformSplit = stop.stop_name.split('Platform') - if (stopSplit.length > 1 && stopSplit[1].trim().length > 0) { - markericon = getMarker('bus', stopSplit[1]) - } else if ( - platformSplit.length > 1 && - platformSplit[1].trim().length > 0 - ) { - markericon = getMarker('bus', platformSplit[1]) - } - } - - // jono's awesome collison detection - // basically checks if something is already there - let lng = stop.stop_lon - if (typeof positionMap[stop.stop_lat] === 'undefined') { - positionMap[stop.stop_lat] = [lng] - } else if (positionMap[stop.stop_lat].indexOf(lng) !== -1) { - lng += 0.0002 - } else { - positionMap[stop.stop_lat].push(lng) - } - return ( - - ) - })} - {bigCircle} - - {stationMarker} - - ) - } - - return ( -
- - - - {mapview} - {offline} -
- ) - } -} -const styles = StyleSheet.create({ - locate: { - backgroundColor: 'rgba(255,255,255,0.9)', - position: 'absolute', - zIndex: 10, - paddingTop: vars.padding * 0.875, - paddingBottom: vars.padding * 0.875, - paddingLeft: vars.padding * 0.375, - paddingRight: vars.padding * 0.375, - top: vars.padding * 0.75, - right: vars.padding * 0.75, - borderRadius: 5, - boxShadow: '0 1px 2px rgba(0,0,0,0.1)', - }, -}) -export default withRouter(BaseMap) diff --git a/js/views/maps/Layer.jsx b/js/views/maps/Layer.jsx deleted file mode 100644 index 7703784a..00000000 --- a/js/views/maps/Layer.jsx +++ /dev/null @@ -1,124 +0,0 @@ -import leaflet from 'leaflet' -import { vars } from '../../styles.js' -import UiStore from '../../stores/UiStore.js' - -const { desktopThreshold } = vars -const GeoJSON = leaflet.geoJSON -const DivIcon = leaflet.divIcon -const Marker = leaflet.marker -const CircleMarker = leaflet.circleMarker - -class Layer { - features = [] - - visible = false - - unmounted = false - - show(bounds = null, dispose = true, hideStops = true, maxZoom = -1) { - if (this.unmounted) { - return - } - if (bounds !== null) { - const options = {} - if (document.documentElement.clientWidth <= desktopThreshold) { - options.paddingBottomRight = [0, 350] - } - UiStore.basemap.fitBounds( - [[bounds.lat_min, bounds.lon_min], [bounds.lat_max, bounds.lon_max]], - options - ) - } - if (this.visible === true) return - if (maxZoom > -1 && dispose === true) { - this.maxZoom = maxZoom - UiStore.basemap.on('zoomend', this.toggleOnZoom) - return this.toggleOnZoom() - } - this.visible = true - this.features.forEach(feature => { - feature.addTo(UiStore.basemap) - }) - if (hideStops) { - UiStore.stopVisibility(hideStops) - } - } - - hide(dispose = true, hideStops = false) { - if (this.visible === false) return - if (this.maxZoom > -1 && dispose === true) { - UiStore.basemap.off('zoomend', this.toggleOnZoom) - } - this.visible = false - this.features.forEach(feature => { - feature.remove(UiStore.basemap) - }) - if (!hideStops) { - UiStore.stopVisibility(hideStops) - } - } - - toggleOnZoom = () => { - // this is pretty primative. need some way of checking the spacing between items - if (this.maxZoom <= UiStore.basemap.getZoom()) { - this.show(null, false) - } else { - this.hide(null, false) - } - } - - add(type, data, props = {}) { - let feature = null - if (type === 'geojson') { - if (props.typeExtension === 'CircleMarker') { - props.pointToLayer = function(feature, latlng) { - return CircleMarker(latlng, props.typeExtensionOptions) - } - } else if (props.typeExtension === 'InvisibleMarker') { - const divIcon = DivIcon({ - iconSize: [48, 48], - className: 'invisible-icon', - }) - props.typeExtensionOptions.icon = divIcon - if (props.typeExtensionOptions.popupContent) { - props.pointToLayer = function(feature, latlng) { - const marker = Marker(latlng, props.typeExtensionOptions).bindPopup( - props.typeExtensionOptions.popupContent(latlng.lat, latlng.lng) - ) - marker.addEventListener( - 'popupopen', - props.typeExtensionOptions.popupOpen - ) - return marker - } - } else { - props.pointToLayer = function(feature, latlng) { - return Marker(latlng, props.typeExtensionOptions) - } - } - } else if (props.icon) { - props.pointToLayer = function(feature, latlng) { - return Marker(latlng, { icon: props.icon }) - } - } - feature = GeoJSON(data, props) - } - if (feature !== null) { - this.features.push(feature) - if (this.visible === true) { - feature.addTo(UiStore.basemap) - } - if (props.order === 'back') { - feature.getLayers().forEach(i => { - requestAnimationFrame(() => i.bringToBack()) - }) - } else if (props.order === 'front') { - feature.getLayers().forEach(i => { - requestAnimationFrame(() => i.bringToFront()) - }) - } - } - } -} - -export default Layer diff --git a/js/views/maps/Mapbox.jsx b/js/views/maps/Mapbox.jsx new file mode 100644 index 00000000..7702c734 --- /dev/null +++ b/js/views/maps/Mapbox.jsx @@ -0,0 +1,192 @@ +import React, { Component } from 'react' +import mapboxgl from 'mapbox-gl' +import { withRouter } from 'react-router' + +import CurrentLocation from '../../stores/CurrentLocation.js' +import SettingsStore from '../../stores/SettingsStore.js' +import StationStore from '../../stores/StationStore.js' +import UiStore from '../../stores/UiStore.js' +import { vars } from '../../styles.js' + +import MapboxStops from './MapboxStops.jsx' +import Layer from './MapboxLayer.jsx' + +const { desktopThreshold } = vars + +// this token can only be used for waka.app +const token = + 'pk.eyJ1IjoiY29uc2luZG8iLCJhIjoiY2tqeXZoMmowMDFpdjJ1cW0zcm90azFtcSJ9.WJ-oKBZImcOI9DagpVHh-Q' +mapboxgl.accessToken = token + +class MapboxMap extends Component { + zoom = 16.5 + + selectedStopLayer = new Layer('selected-stop') + + selectedStopLayerVisible = false + + constructor(props) { + super(props) + this.state = { + online: window.navigator.onLine, + } + } + + componentDidMount() { + window.addEventListener('online', this.triggerRetry) + window.addEventListener('offline', this.triggerOffline) + CurrentLocation.bind('mapmove-silent', this.mapmovesilent) + UiStore.bind('station-data-available', this.stationDataAvailable) + + this.map = new mapboxgl.Map({ + container: this.mapContainer, + style: 'mapbox://styles/consindo/ckjtsal580mwi19o1qyebauwv', + center: SettingsStore.getState() + .lastLocation.slice() + .reverse(), + zoom: this.zoom, + }) + UiStore.state.basemap = this.map + this.map.touchPitch.disable() + this.map.addControl(new mapboxgl.NavigationControl(), 'bottom-left') + this.map.addControl( + new mapboxgl.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true, + }, + trackUserLocation: true, + }) + ) + + // waits for both data & map to load before adding to map + this.loadImages() + this.stops = new MapboxStops(this.map, this.props.history) + } + + componentDidUpdate() { + this.renderSelectedStation() + } + + stationDataAvailable = () => { + this.renderSelectedStation() + } + + loadImages = () => { + const images = [ + 'vehicle-bus', + 'vehicle-train', + 'vehicle-ferry', + 'vehicle-cablecar', + 'vehicle-parkingbuilding', + ] + images.forEach(id => { + this.map.loadImage(`/icons/normal/${id}.png`, (err, image) => { + if (err) return console.error(err) + this.map.addImage(`normal-${id}`, image, { pixelRatio: 2 }) + }) + }) + } + + mapmovesilent = () => { + const position = CurrentLocation.state.position.slice().reverse() + this.map.jumpTo({ + center: position, + zoom: 16.5, + }) + this.renderSelectedStation() + } + + triggerRetry = () => { + this.setState({ + online: window.navigator.onLine, + }) + this.map.jumpTo({ + center: SettingsStore.getState() + .lastLocation.slice() + .reverse(), + zoom: 16.5, + }) + this.renderSelectedStation() + } + + triggerOffline = () => { + this.setState({ + online: false, + }) + } + + renderSelectedStation() { + const { history } = this.props + const splitName = history.location.pathname.split('/') + if (splitName.length === 4 && splitName[1][0] === 's') { + const currentStation = splitName[3] + if (this.selectedStopLayerVisible === currentStation) return + + const item = StationStore.stationCache[currentStation] + if (item === undefined) return + this.selectedStopLayerVisible = currentStation + + const coordinates = [item.stop_lon, item.stop_lat] + const zoom = 17.25 + + // hacks... + this.stops.stopVisibility(false, false) + this.stops.loadStops( + { + lng: item.stop_lon, + lat: item.stop_lat, + }, + zoom + ) + + this.map.flyTo({ + center: coordinates, + zoom, + padding: { + bottom: + document.documentElement.clientWidth <= desktopThreshold ? 350 : 0, + }, + }) + + this.selectedStopLayer.hide() + this.selectedStopLayer.add( + 'geojson', + { + type: 'Point', + coordinates, + }, + { + typeExtension: 'VehicleMarker', + typeExtensionOptions: { + region: splitName[2], + route_type: item.route_type, + size: 1.2, + }, + } + ) + this.selectedStopLayer.show(null, true, false) + } else { + if (!this.selectedStopLayerVisible) return + this.selectedStopLayerVisible = false + this.selectedStopLayer.hide() + } + } + + render() { + const { online } = this.state + return ( +
+
(this.mapContainer = el)} /> + {!online ? ( +
+

You are not connected to the internet.

+ +
+ ) : null} +
+ ) + } +} +export default withRouter(MapboxMap) diff --git a/js/views/maps/MapboxLayer.jsx b/js/views/maps/MapboxLayer.jsx new file mode 100644 index 00000000..35472a9c --- /dev/null +++ b/js/views/maps/MapboxLayer.jsx @@ -0,0 +1,171 @@ +import { getIconName } from './util.jsx' +import UiStore from '../../stores/UiStore.js' +import { vars } from '../../styles.js' + +const { desktopThreshold } = vars + +class MapboxLayer { + id = Math.random().toString() + + visible = false + + mounted = false + + popupCallback = null + + additionalLayers = [] + + constructor(id) { + if (id) { + this.id = id + } + } + + add(type, data, props = {}) { + const map = UiStore.state.basemap + if (type === 'geojson') { + map.addSource(this.id, { + type: 'geojson', + data, + }) + const layer = { + id: this.id, + type: 'symbol', + source: this.id, + layout: { + visibility: 'none', + }, + } + if (data.type === 'LineString') { + layer.type = 'line' + layer.layout['line-join'] = 'round' + layer.layout['line-cap'] = 'round' + layer.paint = { + 'line-color': props.color, + 'line-width': 4, + } + } else if (props.typeExtension === 'CircleMarker') { + layer.type = 'circle' + layer.paint = { + 'circle-color': '#fff', + 'circle-stroke-width': 2, + 'circle-radius': props.typeExtensionOptions.radius, + 'circle-stroke-color': props.typeExtensionOptions.color, + } + } else if (props.typeExtension === 'VehicleMarker') { + layer.layout = { + 'icon-image': getIconName( + props.typeExtensionOptions.region, + props.typeExtensionOptions.route_type, + 'VehicleMarker' + ), + 'icon-ignore-placement': true, + 'icon-size': props.typeExtensionOptions.size || 1, + } + } else { + console.log('add', type, data, props) + } + + if (props.orderBefore && map.getLayer(props.orderBefore) != null) { + map.addLayer(layer, props.orderBefore) + } else { + map.addLayer(layer) + } + + if ((props.typeExtensionOptions || {}).popupContent) { + const id = `${this.id}-popups` + map.addLayer({ + id, + type: 'circle', + source: this.id, + paint: { + 'circle-radius': 15, + 'circle-opacity': 0, + }, + }) + this.additionalLayers.push(id) + + this.popupCallback = props.typeExtensionOptions.popupContent + map.on('click', id, this.popupCallback) + map.on('mouseenter', id, this.setPointer) + map.on('mouseleave', id, this.setBlank) + } + + this.mounted = true + } else { + console.log('add not geojson') + } + } + + setPointer() { + const map = UiStore.state.basemap + map.getCanvas().style.cursor = 'pointer' + } + + setBlank() { + const map = UiStore.state.basemap + map.getCanvas().style.cursor = '' + } + + show(bounds = null, dispose = true, hideStops = true) { + const map = UiStore.state.basemap + if (bounds !== null) { + const options = { + padding: { + top: 20, + bottom: 50, + left: 20, + right: 20, + }, + } + if (document.documentElement.clientWidth <= desktopThreshold) { + options.padding.bottom = 350 + } + map.fitBounds( + [ + [bounds.lon_max, bounds.lat_max], + [bounds.lon_min, bounds.lat_min], + ], + options + ) + } + + if (this.visible === true) return + const allLayers = [this.id, ...this.additionalLayers] + allLayers.forEach(id => map.setLayoutProperty(id, 'visibility', 'visible')) + + if (hideStops) { + UiStore.stopVisibility(hideStops) + } + } + + hide(dispose = true, hideStops = false) { + if (!this.mounted) return + const map = UiStore.state.basemap + + if (map.getLayer(this.id) === undefined) return + + const allLayers = [this.id, ...this.additionalLayers] + if (dispose === true) { + allLayers.forEach(id => map.removeLayer(id)) + map.removeSource(this.id) + } else { + allLayers.forEach(id => map.setLayoutProperty(id, 'visibility', 'none')) + } + + // removes the popups, little bit hacky, but don't really like popups anyway + if (this.popupCallback != null) { + const id = `${this.id}-popups` + map.off('click', id, this.popupCallback) + map.on('mouseenter', id, this.setPointer) + map.on('mouseleave', id, this.setBlank) + this.popupCallback = null + } + + this.visible = false + if (!hideStops) { + UiStore.stopVisibility(hideStops) + } + } +} +export default MapboxLayer diff --git a/js/views/maps/MapboxStops.jsx b/js/views/maps/MapboxStops.jsx new file mode 100644 index 00000000..ec2a92ce --- /dev/null +++ b/js/views/maps/MapboxStops.jsx @@ -0,0 +1,167 @@ +import local from '../../../local' +import SettingsStore from '../../stores/SettingsStore.js' +import StationStore from '../../stores/StationStore.js' +import UiStore from '../../stores/UiStore.js' + +import { getDist, getIconName } from './util.jsx' + +export default class MapboxStops { + map = null + + history = null + + hideStops = false + + mapboxLoaded = false + + position = [...SettingsStore.getState().lastLocation, 16.5] + + constructor(map, history) { + this.map = map + this.history = history + + UiStore.bind('stop-visibility', this.stopVisibility) + + const dataLoad = this.getData(...this.position) + const mapLoad = new Promise((resolve, reject) => { + // this is here so it handles the initial load race condition + map.on('moveend', () => { + this.loadStops() + }) + this.map.on('load', () => { + this.setupStops() + this.mapboxLoaded = true + resolve() + }) + }) + Promise.all([dataLoad, mapLoad]).then(data => { + if (!this.hideStops) { + this.map.getSource('stops').setData(data[0]) + } + }) + } + + setupStops = () => { + const map = this.map + map.addSource('stops', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [], + }, + }) + const layerData = { + id: 'stops', + type: 'symbol', + source: 'stops', + layout: { + 'icon-image': '{icon}', + 'icon-size': { type: 'identity', property: 'icon_size' }, + 'icon-ignore-placement': true, + }, + } + if (map.getLayer('selected-stop') != null) { + map.addLayer(layerData, 'selected-stop') + } else { + map.addLayer(layerData) + } + map.on('click', 'stops', e => { + const { stop_region, stop_id } = e.features[0].properties + this.viewServices(stop_region, stop_id) + }) + map.on('mouseenter', 'stops', () => { + map.getCanvas().style.cursor = 'pointer' + }) + map.on('mouseleave', 'stops', () => { + map.getCanvas().style.cursor = '' + }) + } + + stopVisibility = (state, load = true) => { + if (this.hideStops !== state) { + const map = this.map + this.hideStops = state + + // hide the layer + if (!this.mapboxLoaded) return + if (state === true) { + map.setLayoutProperty('stops', 'visibility', 'none') + } else { + map.setLayoutProperty('stops', 'visibility', 'visible') + if (load) this.loadStops() + } + } + } + + loadStops = async (centerOverride, zoomOverride) => { + const map = this.map + const center = centerOverride || map.getCenter() + const zoom = zoomOverride || map.getZoom() + if (this.hideStops) return + const data = await this.getData(center.lat, center.lng, zoom) + if (!this.hideStops && this.mapboxLoaded) { + this.map.getSource('stops').setData(data) + } + } + + viewServices = (region, station) => { + const history = this.history + const split = history.location.pathname.split('/') + const currentStation = `/s/${region}/${station}` + if (split[1] === 's' && split.length === 4) { + history.replace(currentStation) + } else { + history.push(currentStation) + } + } + + async getData(lat, lon, zoom) { + const dist = getDist(zoom) + this.position = [lat, lon, dist] + + if (zoom <= 14) { + return { + type: 'FeatureCollection', + features: [], + } + } + + try { + const res = await fetch( + `${local.endpoint}/auto/station/search?lat=${lat.toFixed( + 4 + )}&lon=${lon.toFixed(4)}&distance=${dist}` + ) + const data = await res.json() + const features = data.map(stop => { + StationStore.stationCache[stop.stop_id] = stop + return { + type: 'Feature', + properties: { + stop_region: stop.stop_region, + stop_id: stop.stop_id, + stop_name: stop.stop_name, + route_type: stop.route_type, + icon: getIconName( + stop.stop_region, + stop.route_type, + 'stops', + stop.stop_name + ), + icon_size: zoom >= 16 ? 1 : 0.75, + }, + geometry: { + type: 'Point', + coordinates: [stop.stop_lon, stop.stop_lat], + }, + } + }) + return { + type: 'FeatureCollection', + features: features, + } + } catch (error) { + console.log(error) + } + } +} diff --git a/js/views/maps/index.jsx b/js/views/maps/index.jsx deleted file mode 100644 index 5d50d65d..00000000 --- a/js/views/maps/index.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import BaseMap from './BaseMap.jsx' -// i was going to put something in this file. but maybe not yet. -// maybe this will be a toggle between current leaflet and mapboxgljs when we put it in -export default BaseMap diff --git a/js/views/maps/util.jsx b/js/views/maps/util.jsx new file mode 100644 index 00000000..bc40e5ed --- /dev/null +++ b/js/views/maps/util.jsx @@ -0,0 +1,84 @@ +export const getDist = zoom => { + let screensize = document.body.offsetWidth + if (document.body.offsetHeight > screensize) { + screensize = document.body.offsetHeight + } + let dist = Math.ceil(0.2 * screensize) + if (zoom <= 17 && zoom > 16) { + dist = Math.ceil(0.45 * screensize) + } else if (zoom <= 16) { + dist = Math.ceil(0.8 * screensize) + } + // max the api will handle is 1250 + if (dist > 1250) { + dist = 1250 + } + return dist +} + +export const getIconName = (region, routeType, context, stopName) => { + if (region === 'au-syd') { + // TODO! + return 'normal-default' + } + + let prefix = 'normal-' + if (context === 'VehicleMarker') { + prefix = 'normal-vehicle-' + } else if (context === 'SavedStations') { + prefix = '' + } + + // the standard icons + if (routeType === 2) { + return `${prefix}train` + } + + if (routeType === 3) { + // dynamic bus icons + if (context !== 'VehicleMarker' && context !== 'SavedStations') { + const stopSplit = stopName.split('Stop') + const platformSplit = stopName.split('Platform') + let markericon = null + if (stopSplit.length > 1 && stopSplit[1].trim().length > 0) { + markericon = stopSplit[1] + } else if ( + platformSplit.length > 1 && + platformSplit[1].trim().length > 0 + ) { + markericon = platformSplit[1] + } + + if (markericon != null) { + let name = markericon + .trim() + .replace(/\)/g, '') + .replace(/\(/g, '') + if (name.substring(3, 4) === ' ' || name.length === 3) { + name = name.substring(0, 3) + } else if (name.substring(2, 3) === ' ' || name.length === 2) { + name = name.substring(0, 2) + } else { + name = name.substring(0, 1) + } + name = name.replace(/ /g, '').toUpperCase() + + if (['A', 'B', 'C', 'D', 'E', 'F'].includes(name)) { + return `${prefix}bus-${name.toLowerCase()}` + } + } + } + return `${prefix}bus` + } + if (routeType === 4) { + return `${prefix}ferry` + } + if (routeType === 5) { + return `${prefix}cablecar` + } + if (routeType === -1) { + return `${prefix}parkingbuilding` + } + + return `${prefix}default` +} diff --git a/js/views/root/Content.jsx b/js/views/root/Content.jsx index 534ca111..63fbdfeb 100644 --- a/js/views/root/Content.jsx +++ b/js/views/root/Content.jsx @@ -88,7 +88,7 @@ class RootContent extends React.Component { + return iconMap[icon] || } triggerTap = () => { diff --git a/js/views/shell/Content.jsx b/js/views/shell/Content.jsx index cf066b34..00f5359b 100644 --- a/js/views/shell/Content.jsx +++ b/js/views/shell/Content.jsx @@ -21,7 +21,6 @@ import Settings from '../pages/Settings.jsx' import NoMatch from '../pages/NoMatch.jsx' import Wrapper from './Wrapper.jsx' -import AllLines from '../lines/AllLines.jsx' const { desktopThreshold } = vars @@ -101,7 +100,6 @@ class Content extends React.Component { render={wrapFn(Save)} /> - svg path, -.leaflet-tile-container { - pointer-events: none; -} - -.leaflet-marker-icon.leaflet-interactive, -.leaflet-image-layer.leaflet-interactive, -.leaflet-pane > svg path.leaflet-interactive { - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; -} - -/* visual tweaks */ - -.leaflet-container { - background: #ddd; - outline: 0; -} -.leaflet-container a { - color: $lightblue; -} -.leaflet-container a.leaflet-active { - outline: 2px solid orange; -} -.leaflet-zoom-box { - border: 2px dotted #38f; - background: rgba(255, 255, 255, 0.5); -} - -/* general toolbar styles */ - -.leaflet-bar { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(0, 0, 0, 0.02); - border-radius: 3px; -} -.leaflet-bar a, -.leaflet-bar a:hover { - background-color: #fff; - border-bottom: 1px solid #ccc; - width: 26px; - height: 26px; - line-height: 26px; - display: block; - text-align: center; - text-decoration: none; - color: black; -} -.leaflet-bar a, -.leaflet-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; -} -.leaflet-bar a:hover { - background-color: #f4f4f4; -} -.leaflet-bar a:first-child { - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.leaflet-bar a:last-child { - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; - border-bottom: none; -} -.leaflet-bar a.leaflet-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; -} - -.leaflet-touch .leaflet-bar a { - width: 30px; - height: 30px; - line-height: 30px; -} - -/* zoom control */ - -.leaflet-control-zoom-in, -.leaflet-control-zoom-out { - font: bold 18px 'Lucida Console', Monaco, monospace; - text-indent: 1px; -} -.leaflet-control-zoom-out { - font-size: 20px; -} - -.leaflet-touch .leaflet-control-zoom-in { - font-size: 22px; -} -.leaflet-touch .leaflet-control-zoom-out { - font-size: 24px; -} - -/* layers control */ - -// .leaflet-control-layers { -// box-shadow: 0 1px 5px rgba(0,0,0,0.4); -// background: #fff; -// border-radius: 5px; -// } -// .leaflet-control-layers-toggle { -// background-image: url(images/layers.png); -// width: 36px; -// height: 36px; -// } -// .leaflet-retina .leaflet-control-layers-toggle { -// background-image: url(images/layers-2x.png); -// background-size: 26px 26px; -// } -// .leaflet-touch .leaflet-control-layers-toggle { -// width: 44px; -// height: 44px; -// } -// .leaflet-control-layers .leaflet-control-layers-list, -// .leaflet-control-layers-expanded .leaflet-control-layers-toggle { -// display: none; -// } -// .leaflet-control-layers-expanded .leaflet-control-layers-list { -// display: block; -// position: relative; -// } -// .leaflet-control-layers-expanded { -// padding: 6px 10px 6px 6px; -// color: #333; -// background: #fff; -// } -// .leaflet-control-layers-scrollbar { -// overflow-y: scroll; -// padding-right: 5px; -// } -// .leaflet-control-layers-selector { -// margin-top: 2px; -// position: relative; -// top: 1px; -// } -// .leaflet-control-layers label { -// display: block; -// } -// .leaflet-control-layers-separator { -// height: 0; -// border-top: 1px solid #ddd; -// margin: 5px -10px 5px -6px; -// } - -/* attribution and scale controls */ -.leaflet-container .leaflet-control-attribution { - background: rgba(255, 255, 255, 0.9); - margin-bottom: 0; - - @media (max-width: $desktop-threshold) { - display: none; - } -} -@media (min-width: $desktop-threshold + 1) { - .leaflet-control-zoom.leaflet-bar.leaflet-control { - margin-bottom: 25px; - } -} -.leaflet-control-attribution, -.leaflet-control-scale-line { - padding: 0 5px; - color: #333; -} -.leaflet-control-attribution a { - text-decoration: none; -} -.leaflet-control-attribution a:hover { - text-decoration: underline; -} -.leaflet-container .leaflet-control-attribution, -.leaflet-container .leaflet-control-scale { - font-size: 11px; -} -.leaflet-left .leaflet-control-scale { - margin-left: 5px; -} -.leaflet-bottom .leaflet-control-scale { - margin-bottom: 5px; -} -.leaflet-control-scale-line { - border: 2px solid #777; - border-top: none; - line-height: 1.1; - padding: 2px 5px 1px; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - -moz-box-sizing: border-box; - box-sizing: border-box; - - background: #fff; - background: rgba(255, 255, 255, 0.5); -} -.leaflet-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; -} -.leaflet-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; -} - -.leaflet-touch .leaflet-control-attribution, -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - box-shadow: none; -} -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(0, 0, 0, 0.02); - background-clip: padding-box; -} - -/* popup */ - -.leaflet-popup { - position: absolute; - text-align: center; - margin-bottom: 20px; -} -.leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - // border-radius: 12px; -} -.leaflet-popup-content { - margin: 18px; - line-height: 1.4; -} -.leaflet-popup-content p { - margin: 18px 0; -} -.leaflet-popup-tip-container { - width: 40px; - height: 20px; - position: absolute; - left: 50%; - margin-left: -20px; - // overflow: hidden; - pointer-events: none; -} -.leaflet-popup-tip { - width: 17px; - height: 17px; - padding: 1px; - - margin: -11px auto 0; - - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); -} -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: $atblue; - color: #fff; - // box-shadow: 0 3px 14px rgba(0,0,0,0.4); -} -.leaflet-container a.leaflet-popup-close-button { - // position: absolute; - // top: 0; - // right: 0; - // padding: 4px 4px 0 0; - // border: none; - // text-align: center; - // width: 18px; - // height: 14px; - // font: 16px/14px Tahoma, Verdana, sans-serif; - // color: #c3c3c3; - // text-decoration: none; - // font-weight: bold; - // background: transparent; - display: none; -} -.leaflet-container a.leaflet-popup-close-button:hover { - color: #999; -} -.leaflet-popup-scrolled { - overflow: auto; - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; -} -/* div icon */ - -.leaflet-div-icon { - background: #fff; - border: 1px solid #666; -} - -/* Tooltip */ -/* Base styles for the element that has a tooltip */ -.leaflet-tooltip { - position: absolute; - padding: 6px; - background-color: #fff; - border: 1px solid #fff; - border-radius: 3px; - color: #222; - white-space: nowrap; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - pointer-events: none; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); -} -.leaflet-tooltip.leaflet-clickable { - cursor: pointer; - pointer-events: auto; -} -.leaflet-tooltip-top:before, -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - position: absolute; - pointer-events: none; - border: 6px solid transparent; - background: transparent; - content: ''; -} - -/* Directions */ - -.leaflet-tooltip-bottom { - margin-top: 6px; -} -.leaflet-tooltip-top { - margin-top: -6px; -} -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-top:before { - left: 50%; - margin-left: -6px; -} -.leaflet-tooltip-top:before { - bottom: 0; - margin-bottom: -12px; - border-top-color: #fff; -} -.leaflet-tooltip-bottom:before { - top: 0; - margin-top: -12px; - margin-left: -6px; - border-bottom-color: #fff; -} -.leaflet-tooltip-left { - margin-left: -6px; -} -.leaflet-tooltip-right { - margin-left: 6px; -} -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - top: 50%; - margin-top: -6px; -} -.leaflet-tooltip-left:before { - right: 0; - margin-right: -12px; - border-left-color: #fff; -} -.leaflet-tooltip-right:before { - left: 0; - margin-left: -12px; - border-right-color: #fff; -} - -// AT REALTIME EXCLUSIVE CSS -.leaflet-popup-content { - -webkit-tap-highlight-color: transparent; - - span { - display: block; - width: 180px; - text-align: center; - font-size: 13px; - } - img { - max-width: 56px; - } - h2 { - line-height: 1.1; - margin: 10px 0 5px; - letter-spacing: -0.5px; - } - h3 { - color: #ddd; - font-weight: 600; - text-transform: uppercase; - font-size: 1em; - letter-spacing: -0.5px; - margin: 0 0 10px; - } - button { - border: 0; - color: #fff; - width: 100%; - background: $lightblue; - box-sizing: border-box; - padding: 7px 10px; - font-size: 14px; - letter-spacing: -0.5px; - font-weight: 600; - font-family: 'Open Sans', sans-serif; - text-transform: uppercase; - outline: 0; - - &:hover { - background: darken($lightblue, 5); - } - &:active { - background: darken($lightblue, 10); - } - } -} - -.leaflet-marker-icon { - color: transparent; -} diff --git a/scss/_mapbox.scss b/scss/_mapbox.scss new file mode 100644 index 00000000..42ed2eb1 --- /dev/null +++ b/scss/_mapbox.scss @@ -0,0 +1 @@ +.mapboxgl-map{font:12px/20px Helvetica Neue,Arial,Helvetica,sans-serif;overflow:hidden;position:relative;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mapboxgl-canvas{position:absolute;left:0;top:0}.mapboxgl-map:-webkit-full-screen{width:100%;height:100%}.mapboxgl-canary{background-color:salmon}.mapboxgl-canvas-container.mapboxgl-interactive,.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass{cursor:grab;-webkit-user-select:none;user-select:none}.mapboxgl-canvas-container.mapboxgl-interactive.mapboxgl-track-pointer{cursor:pointer}.mapboxgl-canvas-container.mapboxgl-interactive:active,.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass:active{cursor:grabbing}.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas{touch-action:pan-x pan-y}.mapboxgl-canvas-container.mapboxgl-touch-drag-pan,.mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas{touch-action:pinch-zoom}.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas{touch-action:none}.mapboxgl-ctrl-bottom-left,.mapboxgl-ctrl-bottom-right,.mapboxgl-ctrl-top-left,.mapboxgl-ctrl-top-right{position:absolute;pointer-events:none;z-index:2}.mapboxgl-ctrl-top-left{top:0;left:0}.mapboxgl-ctrl-top-right{top:0;right:0}.mapboxgl-ctrl-bottom-left{bottom:0;left:0}.mapboxgl-ctrl-bottom-right{right:0;bottom:0}.mapboxgl-ctrl{clear:both;pointer-events:auto;transform:translate(0)}.mapboxgl-ctrl-top-left .mapboxgl-ctrl{margin:10px 0 0 10px;float:left}.mapboxgl-ctrl-top-right .mapboxgl-ctrl{margin:10px 10px 0 0;float:right}.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl{margin:0 0 10px 10px;float:left}.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl{margin:0 10px 10px 0;float:right}.mapboxgl-ctrl-group{border-radius:4px;background:#fff}.mapboxgl-ctrl-group:not(:empty){box-shadow:0 0 0 2px rgba(0,0,0,.1)}@media (-ms-high-contrast:active){.mapboxgl-ctrl-group:not(:empty){box-shadow:0 0 0 2px ButtonText}}.mapboxgl-ctrl-group button{width:29px;height:29px;display:block;padding:0;outline:none;border:0;box-sizing:border-box;background-color:transparent;cursor:pointer}.mapboxgl-ctrl-group button+button{border-top:1px solid #ddd}.mapboxgl-ctrl button .mapboxgl-ctrl-icon{display:block;width:100%;height:100%;background-repeat:no-repeat;background-position:50%}@media (-ms-high-contrast:active){.mapboxgl-ctrl-icon{background-color:transparent}.mapboxgl-ctrl-group button+button{border-top:1px solid ButtonText}}.mapboxgl-ctrl-attrib-button:focus,.mapboxgl-ctrl-group button:focus{box-shadow:0 0 2px 2px #0096ff}.mapboxgl-ctrl button:disabled{cursor:not-allowed}.mapboxgl-ctrl button:disabled .mapboxgl-ctrl-icon{opacity:.25}.mapboxgl-ctrl button:not(:disabled):hover{background-color:rgba(0,0,0,.05)}.mapboxgl-ctrl-group button:focus:focus-visible{box-shadow:0 0 2px 2px #0096ff}.mapboxgl-ctrl-group button:focus:not(:focus-visible){box-shadow:none}.mapboxgl-ctrl-group button:focus:first-child{border-radius:4px 4px 0 0}.mapboxgl-ctrl-group button:focus:last-child{border-radius:0 0 4px 4px}.mapboxgl-ctrl-group button:focus:only-child{border-radius:inherit}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23999'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23aaa'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath d='M14 5l1 1-9 9-1-1 9-9z' fill='red'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-waiting .mapboxgl-ctrl-icon{animation:mapboxgl-spin 2s linear infinite}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23999'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath d='M14 5l1 1-9 9-1-1 9-9z' fill='red'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23666'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath d='M14 5l1 1-9 9-1-1 9-9z' fill='red'/%3E%3C/svg%3E")}}@keyframes mapboxgl-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}a.mapboxgl-ctrl-logo{width:88px;height:23px;margin:0 0 -4px -4px;display:block;background-repeat:no-repeat;cursor:pointer;overflow:hidden;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='88' height='23' viewBox='0 0 88 23' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd'%3E%3Cdefs%3E%3Cpath id='a' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='b' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='c'%3E%3Crect width='100%25' height='100%25' fill='%23fff'/%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/mask%3E%3Cg opacity='.3' stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23c)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23b' mask='url(%23c)'/%3E%3C/g%3E%3Cg opacity='.9' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/g%3E%3C/svg%3E")}a.mapboxgl-ctrl-logo.mapboxgl-compact{width:23px}@media (-ms-high-contrast:active){a.mapboxgl-ctrl-logo{background-color:transparent;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='88' height='23' viewBox='0 0 88 23' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd'%3E%3Cdefs%3E%3Cpath id='a' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='b' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='c'%3E%3Crect width='100%25' height='100%25' fill='%23fff'/%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/mask%3E%3Cg stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23c)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23b' mask='url(%23c)'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/g%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){a.mapboxgl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='88' height='23' viewBox='0 0 88 23' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd'%3E%3Cdefs%3E%3Cpath id='a' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='b' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='c'%3E%3Crect width='100%25' height='100%25' fill='%23fff'/%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/mask%3E%3Cg stroke='%23fff' stroke-width='3' fill='%23fff'%3E%3Ccircle mask='url(%23c)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23b' mask='url(%23c)'/%3E%3C/g%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/svg%3E")}}.mapboxgl-ctrl.mapboxgl-ctrl-attrib{padding:0 5px;background-color:hsla(0,0%,100%,.5);margin:0}@media screen{.mapboxgl-ctrl-attrib.mapboxgl-compact{min-height:20px;padding:2px 24px 2px 0;margin:10px;position:relative;background-color:#fff;border-radius:12px}.mapboxgl-ctrl-attrib.mapboxgl-compact-show{padding:2px 28px 2px 8px;visibility:visible}.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact-show,.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact-show{padding:2px 8px 2px 28px;border-radius:12px}.mapboxgl-ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-inner{display:none}.mapboxgl-ctrl-attrib-button{display:none;cursor:pointer;position:absolute;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1012 0 6 6 0 10-12 0m5-3a1 1 0 102 0 1 1 0 10-2 0m0 3a1 1 0 112 0v3a1 1 0 11-2 0'/%3E%3C/svg%3E");background-color:hsla(0,0%,100%,.5);width:24px;height:24px;box-sizing:border-box;border-radius:12px;outline:none;top:0;right:0;border:0}.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl-attrib-button,.mapboxgl-ctrl-top-left .mapboxgl-ctrl-attrib-button{left:0}.mapboxgl-ctrl-attrib.mapboxgl-compact-show .mapboxgl-ctrl-attrib-inner,.mapboxgl-ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-button{display:block}.mapboxgl-ctrl-attrib.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button{background-color:rgba(0,0,0,.05)}.mapboxgl-ctrl-bottom-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{bottom:0;right:0}.mapboxgl-ctrl-top-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{top:0;right:0}.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{top:0;left:0}.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{bottom:0;left:0}}@media screen and (-ms-high-contrast:active){.mapboxgl-ctrl-attrib.mapboxgl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' fill='%23fff'%3E%3Cpath d='M4 10a6 6 0 1012 0 6 6 0 10-12 0m5-3a1 1 0 102 0 1 1 0 10-2 0m0 3a1 1 0 112 0v3a1 1 0 11-2 0'/%3E%3C/svg%3E")}}@media screen and (-ms-high-contrast:black-on-white){.mapboxgl-ctrl-attrib.mapboxgl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1012 0 6 6 0 10-12 0m5-3a1 1 0 102 0 1 1 0 10-2 0m0 3a1 1 0 112 0v3a1 1 0 11-2 0'/%3E%3C/svg%3E")}}.mapboxgl-ctrl-attrib a{color:rgba(0,0,0,.75);text-decoration:none}.mapboxgl-ctrl-attrib a:hover{color:inherit;text-decoration:underline}.mapboxgl-ctrl-attrib .mapbox-improve-map{font-weight:700;margin-left:2px}.mapboxgl-attrib-empty{display:none}.mapboxgl-ctrl-scale{background-color:hsla(0,0%,100%,.75);font-size:10px;border:2px solid #333;border-top:#333;padding:0 5px;color:#333;box-sizing:border-box}.mapboxgl-popup{position:absolute;top:0;left:0;display:flex;will-change:transform;pointer-events:none}.mapboxgl-popup-anchor-top,.mapboxgl-popup-anchor-top-left,.mapboxgl-popup-anchor-top-right{flex-direction:column}.mapboxgl-popup-anchor-bottom,.mapboxgl-popup-anchor-bottom-left,.mapboxgl-popup-anchor-bottom-right{flex-direction:column-reverse}.mapboxgl-popup-anchor-left{flex-direction:row}.mapboxgl-popup-anchor-right{flex-direction:row-reverse}.mapboxgl-popup-tip{width:0;height:0;border:10px solid transparent;z-index:1}.mapboxgl-popup-anchor-top .mapboxgl-popup-tip{align-self:center;border-top:none;border-bottom-color:#fff}.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip{align-self:flex-start;border-top:none;border-left:none;border-bottom-color:#fff}.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip{align-self:flex-end;border-top:none;border-right:none;border-bottom-color:#fff}.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip{align-self:center;border-bottom:none;border-top-color:#fff}.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip{align-self:flex-start;border-bottom:none;border-left:none;border-top-color:#fff}.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip{align-self:flex-end;border-bottom:none;border-right:none;border-top-color:#fff}.mapboxgl-popup-anchor-left .mapboxgl-popup-tip{align-self:center;border-left:none;border-right-color:#fff}.mapboxgl-popup-anchor-right .mapboxgl-popup-tip{align-self:center;border-right:none;border-left-color:#fff}.mapboxgl-popup-close-button{position:absolute;right:0;top:0;border:0;border-radius:0 3px 0 0;cursor:pointer;background-color:transparent}.mapboxgl-popup-close-button:hover{background-color:rgba(0,0,0,.05)}.mapboxgl-popup-content{position:relative;background:#fff;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1);padding:10px 10px 15px;pointer-events:auto}.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content{border-top-left-radius:0}.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content{border-top-right-radius:0}.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content{border-bottom-left-radius:0}.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content{border-bottom-right-radius:0}.mapboxgl-popup-track-pointer{display:none}.mapboxgl-popup-track-pointer *{pointer-events:none;user-select:none}.mapboxgl-map:hover .mapboxgl-popup-track-pointer{display:flex}.mapboxgl-map:active .mapboxgl-popup-track-pointer{display:none}.mapboxgl-marker{position:absolute;top:0;left:0;will-change:transform;opacity:1;transition:opacity .2s}.mapboxgl-marker-occluded{opacity:.2}.mapboxgl-user-location-dot,.mapboxgl-user-location-dot:before{background-color:#1da1f2;width:15px;height:15px;border-radius:50%}.mapboxgl-user-location-dot:before{content:"";position:absolute;animation:mapboxgl-user-location-dot-pulse 2s infinite}.mapboxgl-user-location-dot:after{border-radius:50%;border:2px solid #fff;content:"";height:19px;left:-2px;position:absolute;top:-2px;width:19px;box-sizing:border-box;box-shadow:0 0 3px rgba(0,0,0,.35)}@keyframes mapboxgl-user-location-dot-pulse{0%{transform:scale(1);opacity:1}70%{transform:scale(3);opacity:0}to{transform:scale(1);opacity:0}}.mapboxgl-user-location-dot-stale{background-color:#aaa}.mapboxgl-user-location-dot-stale:after{display:none}.mapboxgl-user-location-accuracy-circle{background-color:rgba(29,161,242,.2);width:1px;height:1px;border-radius:100%}.mapboxgl-crosshair,.mapboxgl-crosshair .mapboxgl-interactive,.mapboxgl-crosshair .mapboxgl-interactive:active{cursor:crosshair}.mapboxgl-boxzoom{position:absolute;top:0;left:0;width:0;height:0;background:#fff;border:2px dotted #202020;opacity:.5}@media print{.mapbox-improve-map{display:none}} diff --git a/scss/_search.scss b/scss/_search.scss index ec054362..234c8620 100644 --- a/scss/_search.scss +++ b/scss/_search.scss @@ -4,69 +4,9 @@ } .search { height: 100%; -} -.leaflet-marker-icon.currentSelectionIcon { - background: $atblue; - padding: 7px; - border-radius: 50%; - z-index: 10000 !important; - margin-left: -19px !important; - margin-top: -31px !important; - box-shadow: 0 1px 2px rgba(#000, 0.25); - - &.larger { - margin-left: -22px !important; - margin-top: -32px !important; - padding: 8px; - } -} -.top-button { - bottom: 82px; -} -.bottom-button { - bottom: 14px; -} -.blue-button { - background: $lightblue; - &:hover { - background: darken($lightblue, 5); - } - &:active { - background: darken($lightblue, 10); - } - - svg { - fill: #fff; - } -} -@media (pointer: coarse) { - .leaflet-control-zoom { - display: none; - } -} - -.bigCurrentLocationCircle { - stroke: none; - //opacity: 0.15; - fill: lighten($lightblue, 6); -} - -.smallCurrentLocationCircle { - fill-opacity: 1; - fill: $current-location; - stroke: white; - stroke-width: 2; -} - -.leaflet-marker-icon { - margin-top: -24px !important; -} -.leaflet-popup-content { - svg { - display: block; - position: relative !important; - margin: 0 auto; + @media (max-width: $desktop-threshold) { + height: calc(100% - 58px); } } .offline-container { @@ -74,7 +14,8 @@ box-sizing: border-box; left: 0; right: 0; - bottom: 0; + top: 0; + z-index: 5; background: rgba(#000, 0.85); color: #fff; text-align: center; diff --git a/scss/maps.scss b/scss/maps.scss index 0b2a26bc..5d7ac5f7 100644 --- a/scss/maps.scss +++ b/scss/maps.scss @@ -1,3 +1,113 @@ -@import "_vars"; +@import '_vars'; +@import '_mapbox'; -@import "_leaflet"; \ No newline at end of file +.mapboxgl-map { + height: 100%; +} + +// the zoom & locate controls on the map +.mapboxgl-ctrl-group:not(:empty) { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(0, 0, 0, 0.02); +} + +// our custom icon for the geolocation button +.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon, +.mapboxgl-ctrl + button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background + .mapboxgl-ctrl-icon { + background-image: url(/icons/locate-2.svg); +} +.mapboxgl-ctrl + button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active + .mapboxgl-ctrl-icon { + background-image: url(/icons/locate-2-fill.svg); +} +.mapboxgl-ctrl + button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-waiting + .mapboxgl-ctrl-icon { + animation: waka-mapbox-fade 0.75s ease infinite; +} +.mapboxgl-ctrl-group button.mapboxgl-ctrl-geolocate { + width: 36px; + height: 52px; +} +@keyframes waka-mapbox-fade { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} + +// stops it from overlapping with the bottom bar +@media (max-width: $desktop-threshold) { + .mapboxgl-ctrl-bottom-left, + .mapboxgl-ctrl-bottom-right { + bottom: 8px; + } +} + +// stops popups +.mapbox-stops-popup { + font-size: 13px; + font-family: 'Open Sans', sans-serif; + + &.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip, + &.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip, + &.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip { + border-top-color: $atblue; + } + &.mapboxgl-popup-anchor-top .mapboxgl-popup-tip, + &.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip, + &.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip { + border-bottom-color: $atblue; + } + &.mapboxgl-popup-anchor-left .mapboxgl-popup-tip { + border-right-color: $atblue; + } + &.mapboxgl-popup-anchor-right .mapboxgl-popup-tip { + border-left-color: $atblue; + } + .mapboxgl-popup-content { + background: $atblue; + color: #fff; + text-align: center; + width: 180px; + padding: 18px; + + h2 { + line-height: 1.1; + margin: 0 0 5px; + letter-spacing: -0.5px; + } + h3 { + color: #ddd; + font-weight: 600; + text-transform: uppercase; + font-size: 1em; + letter-spacing: -0.5px; + margin: 0 0 10px; + } + button { + border: 0; + color: #fff; + width: 100%; + background: $lightblue; + box-sizing: border-box; + padding: 7px 10px; + font-size: 14px; + letter-spacing: -0.5px; + font-weight: 600; + text-transform: uppercase; + outline: 0; + + &:active { + opacity: 0.7; + } + } + } +}