diff --git a/src/css/stylish.css b/src/css/stylish.css index 9423524..b58a87a 100644 --- a/src/css/stylish.css +++ b/src/css/stylish.css @@ -12,19 +12,26 @@ body { } .button-container { - margin: 10px auto; width: 90%; + margin: 5%; } .button { - background-color: #4CAF50; /* Green */ + display: block; border: none; + width: 100%; + border-radius: 3px; + background-color: #4CAF50; /* Green */ color: white; - padding: 15px 32px; + padding: 10px; text-align: center; text-decoration: none; - display: inline-block; font-size: 16px; + margin-bottom: 5%; +} + +#hide-filter-button { + background-color: black; } .hidden { @@ -32,10 +39,12 @@ body { } #date-selector-container { + font-size: 16px; display: grid; - justify-content: center; - grid-template-rows: repeat(5, 25px); - grid-template-columns: 25% 25%; + margin-left: 5%; + margin-bottom: 5%; + grid-template-rows: repeat(5, auto); + grid-template-columns: auto auto; grid-auto-flow: column; grid-auto-columns: 50% 50%; } @@ -124,90 +133,6 @@ body { } -/* Modal stuff */ - -.modal { - position: fixed; /* Stay in place */ - z-index: 1; /* Sit on top */ - padding-top: 100px; /* Location of the box */ - left: 0; - top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - overflow: auto; /* Enable scroll if needed */ - background-color: rgb(0,0,0); /* Fallback color */ - background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ -} - -/* Modal Content */ -.modal-content { - position: relative; - background-color: #fefefe; - margin: auto; - padding: 0; - border: 1px solid #888; - width: 80%; - box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); - -webkit-animation-name: animatetop; - -webkit-animation-duration: 0.4s; - animation-name: animatetop; - animation-duration: 0.4s -} - -/* Add Animation */ -@-webkit-keyframes animatetop { - from {top:-300px; opacity:0} - to {top:0; opacity:1} -} - -@keyframes animatetop { - from {top:-300px; opacity:0} - to {top:0; opacity:1} -} - -/* The Close Button */ -.close-filter-modal { - color: white; - float: right; - font-size: 28px; - font-weight: bold; -} - -.close-filter-modal:hover, -.close-filter-modal:focus { - color: #000; - text-decoration: none; - cursor: pointer; -} - -.modal-header { - padding: 5px ; - background-color: #5cb85c; - color: white; - height: 35px; - font-size: 20px; -} - -.modal-body {padding: 2px 16px;} - -.modal-footer { - padding: 2px 16px; - background-color: #5cb85c; - color: white; -} - -#filterModal .buttons { - display: flex; - justify-content: space-evenly; - padding: 20px; - text-align: center; -} - -#filterModal .buttons a { - margin: 20px; -} - - /* Media */ @media only screen and (max-device-width: 580px) { @@ -249,8 +174,24 @@ body { font-size: 64px; } - #filterModal .buttons { - display: flex; - flex-direction: column; - } +} + +/* Checkbox larger */ + +@supports (zoom:2) { + input[type=checkbox]{ + zoom: 1.5; + } +} +@supports not (zoom:2) { + input[type=checkbox]{ + transform: scale(1.5); + margin: 15px; + } +} +label{ + /* fix vertical align issues */ + display: inline-block; + vertical-align: top; + margin-top: 10px; } diff --git a/src/index.html b/src/index.html index 5114862..3817864 100644 --- a/src/index.html +++ b/src/index.html @@ -31,40 +31,28 @@
Filter shows +
- -
+ - - -
- diff --git a/src/js/app.jsx b/src/js/app.jsx index 0c34980..c76e31f 100644 --- a/src/js/app.jsx +++ b/src/js/app.jsx @@ -23,8 +23,14 @@ class Application extends Component { this.toggleCheckbox = this.toggleCheckbox.bind(this); this.state = { dates: [], + filtered: { + date: [], + onscreen: [], + searched: [], + } }; + this.allShows = []; this.onscreenShows = []; this.popup = new mapboxgl.Popup({ @@ -51,6 +57,7 @@ class Application extends Component { parsed.then((data) => { // Set the selected dates this.setState({ dates: data.dates }); + this.allShows = data.geojson.features; // Add the dates const dateEl = document.getElementById('date-selector-container'); @@ -63,11 +70,15 @@ class Application extends Component { // Modals $('#filter-button').on('click', () => { - $('#filterModal').toggleClass('hidden'); + $('#filter-button').toggleClass('hidden'); + $('#hide-filter-button').toggleClass('hidden'); + $('#filters').toggleClass('hidden'); }); - $('.close-filter-modal').on('click', () => { - $('#filterModal').toggleClass('hidden'); + $('#hide-filter-button').on('click', () => { + $('#filter-button').toggleClass('hidden'); + $('#hide-filter-button').toggleClass('hidden'); + $('#filters').toggleClass('hidden'); }); // Mobile only @@ -91,7 +102,7 @@ class Application extends Component { const checkedDatesList = _.map(checkedDates, 'date'); // Filter - const filtered = this.onscreenShows.filter(feature => + this.state.filtered.date = this.allShows.filter(feature => _.includes(checkedDatesList, feature.properties.date)); // Set filter for points @@ -100,10 +111,10 @@ class Application extends Component { // Update source, for clusters this.map.getSource('shows').setData({ type: 'FeatureCollection', - features: filtered, + features: this.state.filtered.date, }); - this.renderListings(this.map, filtered); + this.renderListings(this.map, this.state.filtered.date); } } @@ -143,7 +154,7 @@ class Application extends Component { type: 'circle', source: 'shows', paint: { - 'circle-color': '#11b4da', + 'circle-color': '#4CAF50', 'circle-radius': 10, 'circle-stroke-width': 1, 'circle-stroke-color': '#fff', @@ -183,7 +194,7 @@ class Application extends Component { // Store the current features in sn `onscreenShows` variable to // later use for filtering on `keyup`. - this.onscreenShows = uniqueFeatures; + this.state.filtered.onscreen = uniqueFeatures; } }; @@ -212,7 +223,7 @@ class Application extends Component { } // Filter visible features that don't match the input value. - const filtered = this.onscreenShows.filter((feature) => { + const filtered = this.state.filtered.onscreen.filter((feature) => { const selected = _.includes(getCheckedDatesList(), feature.properties.date); const match = (x) => { const prop = Util.normalize(feature.properties[x]); @@ -234,6 +245,8 @@ class Application extends Component { // Set the filter to populate features into the layer. const filteredShows = filtered.map(feature => feature.properties.sid); map.setFilter('shows', ['in', 'sid'].concat(filteredShows)); + + this.state.filtered.searched = filtered; }); // Call this function on initialization @@ -330,6 +343,7 @@ class Application extends Component { const prop = feature.properties; const item = document.createElement('p'); item.textContent = prop.showString; + const venue = `

${prop.venue}


`; item.addEventListener('mouseover', () => { // Highlight corresponding feature on the map @@ -337,6 +351,7 @@ class Application extends Component { .setHTML(venue + prop.showHTML) .addTo(map); }); + this.listingEl.appendChild(item); }); @@ -346,6 +361,7 @@ class Application extends Component { const empty = document.createElement('p'); const text = this.filterEl.value === '' ? 'Drag the map to populate results' : 'No shows match criteria.'; empty.textContent = text; + this.listingEl.appendChild(empty); // Hide the filter input diff --git a/src/mapper.js b/src/mapper.js new file mode 100644 index 0000000..c0533bc --- /dev/null +++ b/src/mapper.js @@ -0,0 +1,489 @@ +// globals + +var data; +var map; +var resp; +var geojson; +var organized; +var myLayer; +var overlays; +var filters; +var selectedDatesList; +var geoResponse; + +// the scrape + +var urls = "'http://www.foopee.com/punk/the-list/by-date.0.html', 'http://www.foopee.com/punk/the-list/by-date.1.html'"; +var xpath = "//body/ul/li"; +var query = "select * from htmlstring where url in (" + urls + ") and xpath='" + xpath + "'"; +var yql_url = "https://query.yahooapis.com/v1/public/yql?format=json&q=" + encodeURIComponent(query) + "&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys"; + + +//////////// +// foopee / +////////// + +function get(url) { + + // Return a new promise. + return new Promise(function(resolve, reject) { + + var req = new XMLHttpRequest(); + req.open('GET', url); + + req.onload = function() { + if (req.status == 200) { + resp = JSON.parse(req.response); + organized = sortByDate(resp); + resolve(console.log('Request success.'));; + } else { + reject(console.log(Error(req.statusText))); + } + }; + + // Handle network errors + req.onerror = function() { + reject(Error("Network Error")); + }; + + req.send(); + }); +} + + +//////////// +// MAPBOX / +////////// + +// defaults +function ModifiedClusterGroup() { + return new L.MarkerClusterGroup({ + spiderfyOnMaxZoom: true, + maxClusterRadius: 1, + spiderfyDistanceMultiplier: 3 + /* custom icons ? + iconCreateFunction: function(cluster) { + return L.mapbox.marker.icon({ + // show the number of markers in the cluster on the icon. + 'marker-symbol': cluster.getChildCount(), + 'marker-color': '#a0d6b4' + }); + } + */ + }); +} + + +function setupMap() { + // Return a new promise + return new Promise(function(resolve, reject) { + + // easy to change online though if we suspect abuse + L.mapbox.accessToken = 'pk.eyJ1IjoibWV0YXN5biIsImEiOiIwN2FmMDNhNTRhOWQ3NDExODI1MTllMDk1ODc3NTllZiJ9.Bye80QJ4r0RJsKj4Sre6KQ'; + + // Init map + map = L.mapbox.map('map', 'mapbox.dark', { + maxZoom: 17 + }) + .setView([37.7600, -122.416], 13); + + // Locate me button + L.control.locate().addTo(map); + + + if (map) { + resolve(console.log('Map is loaded.')); + } else { + reject(console.log(Error('Map not loaded!'))); + } + }); +} + +// filters + +function populateDates(organized) { + // grab form + var form = document.getElementById('date-selector'); + var dates = Object.keys(organized); + + // lazy + form.innerHTML = '
' + for (var d = 0; d < dates.length; d++) { + var le_radio = " " + dates[d] + form.innerHTML = form.innerHTML + le_radio + } + form.innerHTML += '
' + filters = document.getElementById('date-selector').filters; +} + + + +function showShows() { + + selectedDatesList = []; + // first collect all of the checked boxes and create an array of strings + for (var i = 0; i < filters.length; i++) { + if (filters[i].checked) selectedDatesList.push(filters[i].value); + } + // then remove any previously-displayed marker groups + overlays.clearLayers(); + // create a new marker group + var clusterGroup = ModifiedClusterGroup().addTo(overlays); + // and add any markers that fit the filtered criteria to that group. + myLayer.eachLayer(function(layer) { + if (selectedDatesList.indexOf(layer.feature.properties.date) !== -1) { + clusterGroup.addLayer(layer); + } + }); + + // update coordinates box + onmove(); + +} + + + + +///////////// +// helpers / +/////////// + +function parseHTMLToDOM(j){ + var res = j['query']['results']['result']; + var results = res.join('\n'); + + // array of dates + p = new DOMParser(); + results = p.parseFromString(results, 'text/html'); + $results = $(results); + + return $results; +} + +function sortByDate(j) { + + $results = parseHTMLToDOM(j); + console.log($results) + + // grab the dates to use as keys + dates = $results.find('body > li > a').map(function() { + return $.trim(this.text); + }).get(); + + organized = {}; + + console.log(dates.length) + + for (var i = 0; i < dates.length; i++) { + + // empty date + organized[dates[i]] = []; + + // Array is zero indexed but nth-child starts at 1 + var index = i + 1 + var $shows = $results.find('body > li:nth-child(' + index + ')').find('li'); + + for (var si = 0; si < $shows.length; si++) { + + // god save us all, i'm so sorry + var things= $($shows[si]).find('a').map(function() { + return $.trim(this.text); + }).get(); + + // really, I am + var venue = things.shift(); + var bands = things; + var deets = $.trim($shows[si].innerText.split('\n').slice(-3, -2)); + + organized[dates[i]].push({ + 'venue': venue, + 'date': dates[i], + 'details': deets, + 'bands': bands, + }); + } + } + + // lol "organized" + return organized; +} + + +// Compute the edit distance between the two given strings +function getEditDistance(a, b) { + if (a.length === 0) return b.length; + if (b.length === 0) return a.length; + + var matrix = []; + + // increment along the first column of each row + var i; + for (i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + + // increment each column in the first row + var j; + for (j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (i = 1; i <= b.length; i++) { + for (j = 1; j <= a.length; j++) { + 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 + } + } + } + + return matrix[b.length][a.length]; +}; + + +function geojsonify(data) { + // this function returns a geojson object + + var features = [] + var dateKeys = Object.keys(data) + + // loop through dates + for (var i = 0; i < dateKeys.length; i++) { + + // loop through shows + for (var j = 0; j < data[dateKeys[i]].length; j++) { + + + var showData = data[dateKeys[i]][j]; + var venueList = Object.keys(lonlatDictionary); + + // check for misspellings + if (!lonlatDictionary[showData['venue']]) { + try { + for (var v = 0; v < venueList.length; v++) { + var misspelled = showData['venue'].replace(/\W/g, '') + var spelledCorrect = venueList[v].replace(/\W/g, '') + var 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); + } + } + + var show = { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": lonlatDictionary[showData['venue']] || [-122.422960, 37.826524] + }, + "properties": { + "date": dateKeys[i], + "venue": showData['venue'], + "bands": showData['bands'], + "details": showData['details'].replace(/ ,/g, ''), // fucking commas + 'marker-color': '#33CC33', //+Math.floor(Math.random()*16777215).toString(16), //random colors ! + 'marker-size': 'large', + 'marker-symbol': 'music' + } + } + + // add show to features array + features.push(show) + + } + } + + // format for valid geojson + var geojson = { + "type": "FeatureCollection", + "features": features + } + return geojson +} + + + +function plotShows(json) { + + return new Promise(function(resolve, reject) { + + + // update function for coordinates infobox + window.onmove = function onmove() { + // Get the map bounds - the top-left and bottom-right locations. + var inBounds = [], + bounds = map.getBounds(); + clusterGroup.eachLayer(function(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) { + var feature = marker.feature; + var 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'); + } + + + // get that geojson + geojson = geojsonify(organized); + + // attach data + myLayer = L.mapbox.featureLayer(geojson) + + // make clustergroup + var 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(function(e) { + + var marker = e; + var feature = e.feature; + + // Create custom popup content + var 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(); + + + if (geojson) { + resolve(console.log('Shows plotted.')) + } else { + reject(console.log(Error('Shows cannot be plotted.'))); + } + }); +} + + +function toggleDate(desc) { + for (var i = 0; i < filters.length; i++) { + + if (desc == 'today') { + var day = Date().slice(0, 10) // this gives us the foopee time format + } else if (desc == 'tomorrow') { + var d = new Date(); + var day = new Date(((d.getTime() / 1000) + (60 * 60 * 24)) * 1000); // milliseconds not seconds + day = day.toString().slice(0, 10); + } + + // lol, so foopee puts its date with no zero padding: + if (day) { + var day_list = day.split(' '); + day_list[2] = String(parseInt(day_list[2])); + day = day_list.join(' '); + } + + if (filters[i].value == day) { + filters[i].checked = 1; + } else { + filters[i].checked = 0; + } + + if (desc == 'all') { + filters[i].checked = 1 + } + } + + // update + showShows(); + +} +//////////////// +// vex modal // +/////////////// + +vex.defaultOptions.className = 'vex-theme-flat-attack'; + + +function modalPop() { + var modalMessage = $('#modal-template').html(); + $('#q').on("click hover", vex.dialog.alert(modalMessage)) +} + + +/////////////////// +// control logic / +///////////////// + +get(yql_url).then(function(resolve) { + try { + setupMap(); + } catch (err) { + vex.dialog.alert('OH SHIT SOMETHINGS BROKEN. The List could be down, rawgit could be mad, or my code could be broken.') + } + populateDates(organized); + plotShows(resp); + modalPop(); +}) + + + +/////////////// +// gmaps api / +///////////// + +// Note: I don't think I want to use these because they were pretty inacurate when using the venue descriptions +// from foopee. But I'm going to leave them here in case they get used as a catchall once the locations are all +// added to the lonlat dictionary. + + +function fetchGeo(venue) { + + return new Promise(function(resolve, reject) { + + // api key + var apiKey = "AIzaSyDCyj1LQMqFPcQhgfW92vR8BtXhlDIvF-4"; + // request + var geocoder = "https://maps.googleapis.com/maps/api/geocode/json?address=" + encodeURIComponent(venue) + "&key=" + apiKey; + + //clear + geoResponse = ''; + + $.getJSON(geocoder, function(response) { + + if (response) { + geoResponse = response; + resolve(console.log('Looked up venue.')) + } else { + reject(console.log(Error('Venue lookup failure.'))); + } + }) + }) +} + +function getLonLat(venue) { + + fetchGeo(venue).then(function(resolve) { + geoResponse = [geoResponse.results[0].geometry.location.lng, geoResponse.results[0].geometry.location.lat] + }) +}