diff --git a/package.json b/package.json index e73fd9b..2e58551 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "jquery": "^3.3.1", "mapbox-gl": "^0.44.0", "react": "^16.2.0", - "react-dom": "^16.2.0" + "react-dom": "^16.2.0", + "prop-types": "^15.6.0" } } diff --git a/src/css/stylish.css b/src/css/stylish.css index 2353ab7..ae091de 100644 --- a/src/css/stylish.css +++ b/src/css/stylish.css @@ -97,3 +97,21 @@ button { .vex-content h1 { font-size: 32px; } + +/* Mapbox GL Stuff */ + +.marker { + background-image: url('../img/marker.png'); + background-size: contain; + width: 30px; + height: 77px; + cursor: pointer; +} + +.mapboxgl-popup { + max-width: 200px; +} + +.mapboxgl-popup-content { + font-family: 'Open Sans', sans-serif; +} diff --git a/src/img/marker.png b/src/img/marker.png new file mode 100644 index 0000000..4c87bec Binary files /dev/null and b/src/img/marker.png differ diff --git a/src/js/app.jsx b/src/js/app.jsx index 8e7f4e4..61d6c09 100644 --- a/src/js/app.jsx +++ b/src/js/app.jsx @@ -1,25 +1,36 @@ import React, { Component } from 'react'; import { render } from 'react-dom'; -import Parser from './components/parser'; -import Util from './components/util'; -import ShowMap from './components/map'; +import Parser from './components/Parser'; +import Dates from './components/dates/Dates'; +import ShowMap from './components/Map'; import '../css/stylish.css'; class Application extends Component { - static plot() { + static prepare() { const parsed = new Parser().parseData(); + // keys: organized, dates parsed.then((data) => { - Util.populateDates(data.dates); + // Pop modal? + // TODO + + // Add the dates + const dateEl = document.getElementById('date-selector-container'); + render(, dateEl); + + // Add the shows + const mapEl = document.getElementById('app'); + render(, mapEl); + console.log(data.geojson) }); } render() { - Application.plot(); + Application.prepare(); return ( - +
); } } diff --git a/src/js/components/Plotter.js b/src/js/components/Plotter.js new file mode 100644 index 0000000..d2ba40f --- /dev/null +++ b/src/js/components/Plotter.js @@ -0,0 +1,69 @@ +export default class Plotter { + static plotShows(organized) { + return organized; + } +} + +/* +function plotShows(geojson) { + // update function for coordinates infobox + window.onmove = function onmove() { + // Get the map bounds - the top-left and bottom-right locations. + const inBounds = [], + bounds = map.getBounds(); + clusterGroup.eachLayer((marker) => { + // For each marker, consider whether it is currently visible by comparing + // with the current map bounds. + if (bounds.contains(marker.getLatLng()) && selectedDatesList.indexOf(marker.feature.properties.date) !== -1) { + const feature = marker.feature; + const coordsTemplate = L.mapbox.template('{{properties.date}} - {{properties.venue}} |{{#properties.bands}} {{.}} |{{/properties.bands}}{{properties.details}}', feature); + inBounds.push(coordsTemplate); + } + }); + // Display a list of markers. + inBounds.reverse(); + document.getElementById('coordinates').innerHTML = inBounds.join('\n'); + }; + + // attach data + const myLayer = L.mapbox.featureLayer(geojson); + + // make clustergroup + const clusterGroup = ModifiedClusterGroup(); + // add features + clusterGroup.addLayer(myLayer); + overlays = L.layerGroup().addTo(map); + // add cluster layer + // overlays are multiple layers + // add in showShows() + showShows(); + + // for each layer in feature layer + myLayer.eachLayer((e) => { + const marker = e; + const feature = e.feature; + + // Create custom popup content + const popupContent = L.mapbox.template('

{{properties.venue}}


{{properties.date}}


{{#properties.bands}} - {{.}}
{{/properties.bands}}


{{properties.details}}


', feature); + + marker.bindPopup(popupContent, { + closeButton: true, + minWidth: 320, + }); + }); + + + map.on('move', onmove); + // call onmove off the bat so that the list is populated. + // otherwise, there will be no markers listed until the map is moved. + window.onmove(); +} + +function ModifiedClusterGroup() { + return new L.MarkerClusterGroup({ + spiderfyOnMaxZoom: true, + maxClusterRadius: 1, + spiderfyDistanceMultiplier: 3, + }); +} +*/ diff --git a/src/js/components/dates/DateSelector.jsx b/src/js/components/dates/DateSelector.jsx new file mode 100644 index 0000000..59b7cc2 --- /dev/null +++ b/src/js/components/dates/DateSelector.jsx @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +export default class DateSelector extends Component { + render() { + return ( +
+ { this.props.date } +
+ ); + } +} + +DateSelector.propTypes = { + date: PropTypes.string.isRequired, + showShows: PropTypes.func.isRequired, +}; + diff --git a/src/js/components/dates/Dates.jsx b/src/js/components/dates/Dates.jsx new file mode 100644 index 0000000..23c5422 --- /dev/null +++ b/src/js/components/dates/Dates.jsx @@ -0,0 +1,33 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import DateSelector from './DateSelector'; + +export default class Dates extends Component { + constructor(props) { + super(props); + this.makeDateSelectors = this.makeDateSelectors.bind(this); + this.showShows = this.showShows.bind(this); + } + + // eslint-disable-next-line + showShows(){} + + makeDateSelectors(dates) { + const selectors = []; + for (let d = 0; d < dates.length; d += 1) { + const selector = (); + selectors.push(selector); + } + return selectors; + } + render() { return this.makeDateSelectors(this.props.dates); } +} + +Dates.propTypes = { + dates: PropTypes.array.isRequired, +}; diff --git a/src/js/components/map.jsx b/src/js/components/map.jsx index 794167d..426deec 100644 --- a/src/js/components/map.jsx +++ b/src/js/components/map.jsx @@ -1,13 +1,16 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import mapboxgl from 'mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; +import Venues from '../../data/venues.json'; + mapboxgl.accessToken = 'pk.eyJ1IjoibWV0YXN5biIsImEiOiIwN2FmMDNhNTRhOWQ3NDExODI1MTllMDk1ODc3NTllZiJ9.Bye80QJ4r0RJsKj4Sre6KQ'; class ShowMap extends Component { - constructor(Props) { - super(Props); + constructor(props) { + super(props); // Centered on SF this.state = { @@ -15,6 +18,8 @@ class ShowMap extends Component { lat: 37.76, zoom: 13, }; + + this.bindMap = this.bindMap.bind(this); } componentDidMount() { @@ -27,6 +32,7 @@ class ShowMap extends Component { zoom, }); + // Add locator control map.addControl(new mapboxgl.GeolocateControl({ positionOpionts: { enableHighAccuracy: true, @@ -34,26 +40,64 @@ class ShowMap extends Component { trackUserLocation: true, })); + // Add the actual shows + map.on('load', () => { + map.addSource('shows', { + type: 'geojson', + data: this.props.geojson, + }); + }); + map.on('move', () => { - const { lng, lat } = map.getCenter(); + const center = map.getCenter(); this.setState({ - lng: lng.toFixed(4), - lat: lat.toFixed(4), + lng: center.lng.toFixed(4), + lat: center.lat.toFixed(4), zoom: map.getZoom().toFixed(2), }); }); + + ShowMap.plotMarkers(this.props.geojson, map); } - render() { - const { lng, lat, zoom } = this.state; + bindMap(el) { + this.mapContainer = el; + } + static plotMarkers(geojson, map) { + geojson.features.forEach((marker) => { + const el = document.createElement('div'); + el.className = 'marker'; + + const lngLat = Venues[marker.geometry.coordinates]; + const popup = new mapboxgl.Popup({ offset: 25 }).setHTML(marker.properties.showHTML) + + if (lngLat) { + try { + new mapboxgl.Marker(el) + .setLngLat(lngLat) + .setPopup(popup) + .addTo(map); + } catch (e) { + console.log(e) + } + } + }); + } + + + render() { return (
-
this.mapContainer = el} className="absolute top right left bottom" /> +
); } } +ShowMap.propTypes = { + geojson: PropTypes.object.isRequired, +}; + export default ShowMap; diff --git a/src/js/components/parser.js b/src/js/components/parser.js index 95eb3b6..bf7e730 100644 --- a/src/js/components/parser.js +++ b/src/js/components/parser.js @@ -1,5 +1,8 @@ import $ from 'jquery'; +import Venues from '../../data/venues.json'; +import getEditDistance from './Util'; + export default class Parser { constructor() { this.yql = Parser.makeYQL(); @@ -11,7 +14,8 @@ export default class Parser { const results = Parser.parseHTMLtoDOM(success); const dates = Parser.getDates(results); const organized = Parser.sortByDate(results, dates); - return { organized, dates }; + const geojson = Parser.geojsonify(organized); + return { organized, geojson, dates }; }) .catch(e => Error((e))); } @@ -68,4 +72,64 @@ export default class Parser { return organized; } + + static geojsonify(data) { + const features = []; + const dateKeys = Object.keys(data); + + // loop through dates + for (let i = 0; i < dateKeys.length; i += 1) { + // loop through shows + for (let j = 0; j < data[dateKeys[i]].length; j += 1) { + const showData = data[dateKeys[i]][j]; + const venueList = Object.keys(Venues); + + // check for misspellings + if (!Venues[showData.venue]) { + try { + for (let v = 0; v < venueList.length; v += 1) { + const misspelled = showData.venue.replace(/\W/g, ''); + const spelledCorrect = venueList[v].replace(/\W/g, ''); + const editDistance = getEditDistance(misspelled, spelledCorrect); + if (editDistance <= 3) { + console.log(`"${showData.venue}" has been replaced with "${venueList[v]}"`); + showData.venue = venueList[v]; + } + } + } catch (e) { + console.log('Missing Venue?', e); + } + } + + const showString = `${dateKeys[i]} - ${showData.venue} | ${showData.bands.join(' |')} | ${showData.details}`; + const showHTML = `

${showData.venue}


${dateKeys[i]}


${showData.bands.join(' |')}
${showData.details}`; + + const show = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [showData.venue] || [-122.422960, 37.826524], + }, + properties: { + date: dateKeys[i], + venue: showData.venue, + bands: showData.bands, + details: showData.details.replace(/ ,/g, ''), // fucking commas + showString, + showHTML, + }, + }; + + // add show to features array + features.push(show); + } + } + + // format for valid geojson + const geojson = { + type: 'FeatureCollection', + features, + }; + return geojson; + } } diff --git a/src/js/components/util.js b/src/js/components/util.js index 2cf1c3c..9dc180c 100644 --- a/src/js/components/util.js +++ b/src/js/components/util.js @@ -1,13 +1,38 @@ -export default class Util { - static populateDates(dates) { - const form = document.getElementById('date-selector'); - form.innerHTML = '
'; - for (let d = 0; d < dates.length; d += 1) { - const radio = ` ${dates[d]}`; - form.innerHTML += radio; +// Compute the edit distance between the two given strings +export default function getEditDistance(a, b) { + if (a.length === 0) return b.length; + if (b.length === 0) return a.length; + + const matrix = []; + + // increment along the first column of each row + let i; + for (i = 0; i <= b.length; i += 1) { + matrix[i] = [i]; + } + + // increment each column in the first row + let j; + for (j = 0; j <= a.length; j += 1) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (i = 1; i <= b.length; i += 1) { + for (j = 1; j <= a.length; j += 1) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + Math.min( + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1, + ), + ); // deletion + } } - form.innerHTML += '
'; - // TODO: handle filters - // filters = document.getElementById('date-selector').filters; } + + return matrix[b.length][a.length]; }