mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 05:18:34 +03:00
Ingesting config through Create React App
This commit is contained in:
@@ -1,22 +1,23 @@
|
||||
import moment from 'moment'
|
||||
import hash from 'object-hash'
|
||||
import moment from "moment";
|
||||
import hash from "object-hash";
|
||||
|
||||
let { DATE_FMT, TIME_FMT } = process.env
|
||||
if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY'
|
||||
if (!TIME_FMT) TIME_FMT = 'HH:mm'
|
||||
let { DATE_FMT, TIME_FMT } = process.env;
|
||||
if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY";
|
||||
if (!TIME_FMT) TIME_FMT = "HH:mm";
|
||||
|
||||
export const language = process.env.store.app.language || 'en-US'
|
||||
console.log(process.env);
|
||||
export const language = process.env.store.app.language || "en-US";
|
||||
|
||||
export function calcDatetime (date, time) {
|
||||
if (!time) time = '00:00'
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`)
|
||||
return dt.toDate()
|
||||
export function calcDatetime(date, time) {
|
||||
if (!time) time = "00:00";
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`);
|
||||
return dt.toDate();
|
||||
}
|
||||
|
||||
export function getCoordinatesForPercent (radius, percent) {
|
||||
const x = radius * Math.cos(2 * Math.PI * percent)
|
||||
const y = radius * Math.sin(2 * Math.PI * percent)
|
||||
return [x, y]
|
||||
export function getCoordinatesForPercent(radius, percent) {
|
||||
const x = radius * Math.cos(2 * Math.PI * percent);
|
||||
const y = radius * Math.sin(2 * Math.PI * percent);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,32 +27,33 @@ export function getCoordinatesForPercent (radius, percent) {
|
||||
*
|
||||
* Return value:
|
||||
* ex. {'#fff': 0.5, '#000': 0.5, ...} */
|
||||
export function zipColorsToPercentages (colors, percentages) {
|
||||
if (colors.length < percentages.length) throw new Error('You must declare an appropriate number of filter colors')
|
||||
export function zipColorsToPercentages(colors, percentages) {
|
||||
if (colors.length < percentages.length)
|
||||
throw new Error("You must declare an appropriate number of filter colors");
|
||||
|
||||
return percentages.reduce((map, percent, idx) => {
|
||||
map[colors[idx]] = percent
|
||||
return map
|
||||
}, {})
|
||||
map[colors[idx]] = percent;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URI params to start with predefined set of
|
||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
* @param {string} name: name of paramater to search
|
||||
* @param {string} url: url passed as variable, defaults to window.location.href
|
||||
*/
|
||||
export function getParameterByName (name, url) {
|
||||
if (!url) url = window.location.href
|
||||
name = name.replace(/[[\]]/g, `\\$&`)
|
||||
* Get URI params to start with predefined set of
|
||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
* @param {string} name: name of paramater to search
|
||||
* @param {string} url: url passed as variable, defaults to window.location.href
|
||||
*/
|
||||
export function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[[\]]/g, `\\$&`);
|
||||
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
|
||||
const results = regex.exec(url)
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
||||
const results = regex.exec(url);
|
||||
|
||||
if (!results) return null
|
||||
if (!results[2]) return ''
|
||||
if (!results) return null;
|
||||
if (!results[2]) return "";
|
||||
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '))
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,32 +61,35 @@ export function getParameterByName (name, url) {
|
||||
* @param {array} arr1: array of numbers
|
||||
* @param {array} arr2: array of numbers
|
||||
*/
|
||||
export function areEqual (arr1, arr2) {
|
||||
return ((arr1.length === arr2.length) && arr1.every((element, index) => {
|
||||
return element === arr2[index]
|
||||
}))
|
||||
export function areEqual(arr1, arr2) {
|
||||
return (
|
||||
arr1.length === arr2.length &&
|
||||
arr1.every((element, index) => {
|
||||
return element === arr2[index];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the variable is neither null nor undefined
|
||||
* @param {object} variable
|
||||
*/
|
||||
export function isNotNullNorUndefined (variable) {
|
||||
return (typeof variable !== 'undefined' && variable !== null)
|
||||
* Return whether the variable is neither null nor undefined
|
||||
* @param {object} variable
|
||||
*/
|
||||
export function isNotNullNorUndefined(variable) {
|
||||
return typeof variable !== "undefined" && variable !== null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
|
||||
*/
|
||||
export function capitalize (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
|
||||
*/
|
||||
export function capitalize(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
export function trimAndEllipse (string, stringNum) {
|
||||
export function trimAndEllipse(string, stringNum) {
|
||||
if (string.length > stringNum) {
|
||||
return string.substring(0, 120) + '...'
|
||||
return string.substring(0, 120) + "...";
|
||||
}
|
||||
return string
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,71 +99,72 @@ export function trimAndEllipse (string, stringNum) {
|
||||
* through every association's given path attribute to find its location.
|
||||
*
|
||||
* Returns the list of parents: ex. ['Chemical', 'Tear Gas', ...]
|
||||
*/
|
||||
export function getFilterParents (associations, filter) {
|
||||
*/
|
||||
export function getFilterParents(associations, filter) {
|
||||
for (let a of associations) {
|
||||
const { filter_paths: fp } = a
|
||||
const { filter_paths: fp } = a;
|
||||
if (a.id === filter) {
|
||||
return fp.slice(0, fp.length - 1)
|
||||
return fp.slice(0, fp.length - 1);
|
||||
}
|
||||
const filterIndex = fp.indexOf(filter)
|
||||
if (filterIndex === 0) return []
|
||||
if (filterIndex > 0) return fp.slice(0, filterIndex)
|
||||
const filterIndex = fp.indexOf(filter);
|
||||
if (filterIndex === 0) return [];
|
||||
if (filterIndex > 0) return fp.slice(0, filterIndex);
|
||||
}
|
||||
throw new Error('Attempted to get parents of nonexistent filter')
|
||||
throw new Error("Attempted to get parents of nonexistent filter");
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the second to last element in the paths array for a given existing filter.
|
||||
* This is the filter's most immediate ancestor.
|
||||
*/
|
||||
export function getImmediateFilterParent (associations, filter) {
|
||||
const parents = getFilterParents(associations, filter)
|
||||
if (parents.length === 0) return null
|
||||
return parents[parents.length - 1]
|
||||
*/
|
||||
export function getImmediateFilterParent(associations, filter) {
|
||||
const parents = getFilterParents(associations, filter);
|
||||
if (parents.length === 0) return null;
|
||||
return parents[parents.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab a meta filter's siblings, by way of the the `filter_path` hierarcy.
|
||||
*/
|
||||
export function getMetaFilterSiblings (allFilters, filterParent, filterKey) {
|
||||
const idxParent = allFilters.map(f => {
|
||||
return f.filter_paths.reduceRight((acc, path, idx) => {
|
||||
if (path === filterParent) return f.filter_paths[idx + 1]
|
||||
return acc
|
||||
}, null)
|
||||
})
|
||||
.filter(metaFilter => !!metaFilter && metaFilter !== filterKey)
|
||||
return [ ...(new Set(idxParent)) ]
|
||||
*/
|
||||
export function getMetaFilterSiblings(allFilters, filterParent, filterKey) {
|
||||
const idxParent = allFilters
|
||||
.map((f) => {
|
||||
return f.filter_paths.reduceRight((acc, path, idx) => {
|
||||
if (path === filterParent) return f.filter_paths[idx + 1];
|
||||
return acc;
|
||||
}, null);
|
||||
})
|
||||
.filter((metaFilter) => !!metaFilter && metaFilter !== filterKey);
|
||||
return [...new Set(idxParent)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs a given filter's siblings: the set of associations that share the same immediate filter parent.
|
||||
*/
|
||||
export function getFilterSiblings (allFilters, filterParent, filterKey) {
|
||||
const isMetaFilter = !allFilters.map(filt => filt.id).includes(filterKey)
|
||||
*/
|
||||
export function getFilterSiblings(allFilters, filterParent, filterKey) {
|
||||
const isMetaFilter = !allFilters.map((filt) => filt.id).includes(filterKey);
|
||||
|
||||
if (isMetaFilter) {
|
||||
return getMetaFilterSiblings(allFilters, filterParent, filterKey)
|
||||
return getMetaFilterSiblings(allFilters, filterParent, filterKey);
|
||||
}
|
||||
|
||||
return allFilters.reduce((acc, val) => {
|
||||
const valParent = getImmediateFilterParent(allFilters, val.id)
|
||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id)
|
||||
return acc
|
||||
}, [])
|
||||
const valParent = getImmediateFilterParent(allFilters, val.id);
|
||||
if (valParent === filterParent && val.id !== filterKey) acc.push(val.id);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function getEventCategories (event, categories) {
|
||||
const matchedCategories = []
|
||||
export function getEventCategories(event, categories) {
|
||||
const matchedCategories = [];
|
||||
if (event.associations && event.associations.length > 0) {
|
||||
event.associations.reduce((acc, val) => {
|
||||
const foundCategory = categories.find(cat => cat.id === val)
|
||||
if (foundCategory) acc.push(foundCategory)
|
||||
return acc
|
||||
}, matchedCategories)
|
||||
const foundCategory = categories.find((cat) => cat.id === val);
|
||||
if (foundCategory) acc.push(foundCategory);
|
||||
return acc;
|
||||
}, matchedCategories);
|
||||
}
|
||||
return matchedCategories
|
||||
return matchedCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,186 +173,201 @@ export function getEventCategories (event, categories) {
|
||||
* source, call with two sets of parentheses:
|
||||
* const src = insetSourceFrom(sources)(anEvent)
|
||||
*/
|
||||
export function insetSourceFrom (allSources) {
|
||||
export function insetSourceFrom(allSources) {
|
||||
return (event) => {
|
||||
let sources
|
||||
let sources;
|
||||
if (!event.sources) {
|
||||
sources = []
|
||||
sources = [];
|
||||
} else {
|
||||
sources = event.sources.map(id => {
|
||||
return allSources.hasOwnProperty(id) ? allSources[id] : null
|
||||
})
|
||||
sources = event.sources.map((id) => {
|
||||
return allSources.hasOwnProperty(id) ? allSources[id] : null;
|
||||
});
|
||||
}
|
||||
return {
|
||||
...event,
|
||||
sources
|
||||
}
|
||||
}
|
||||
sources,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Debugging function: put in place of a mapStateToProps function to
|
||||
* view that source modal by default
|
||||
*/
|
||||
export function injectSource (id) {
|
||||
return state => {
|
||||
export function injectSource(id) {
|
||||
return (state) => {
|
||||
return {
|
||||
...state,
|
||||
app: {
|
||||
...state.app,
|
||||
source: state.domain.sources[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
source: state.domain.sources[id],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function urlFromEnv (ext) {
|
||||
export function urlFromEnv(ext) {
|
||||
if (process.env[ext]) {
|
||||
if (!Array.isArray(process.env[ext])) { return [`${process.env.SERVER_ROOT}${process.env[ext]}`] } else {
|
||||
return process.env[ext].map(suffix => `${process.env.SERVER_ROOT}${suffix}`)
|
||||
if (!Array.isArray(process.env[ext])) {
|
||||
return [`${process.env.SERVER_ROOT}${process.env[ext]}`];
|
||||
} else {
|
||||
return process.env[ext].map(
|
||||
(suffix) => `${process.env.SERVER_ROOT}${suffix}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleFlagAC (flag) {
|
||||
export function toggleFlagAC(flag) {
|
||||
return (appState) => ({
|
||||
...appState,
|
||||
flags: {
|
||||
...appState.flags,
|
||||
[flag]: !appState.flags[flag]
|
||||
}
|
||||
})
|
||||
[flag]: !appState.flags[flag],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function selectTypeFromPath (path) {
|
||||
let type
|
||||
export function selectTypeFromPath(path) {
|
||||
let type;
|
||||
switch (true) {
|
||||
case /\.(png|jpg)$/.test(path):
|
||||
type = 'Image'; break
|
||||
type = "Image";
|
||||
break;
|
||||
case /\.(mp4)$/.test(path):
|
||||
type = 'Video'; break
|
||||
type = "Video";
|
||||
break;
|
||||
case /\.(md)$/.test(path):
|
||||
type = 'Text'; break
|
||||
type = "Text";
|
||||
break;
|
||||
default:
|
||||
type = 'Unknown'; break
|
||||
type = "Unknown";
|
||||
break;
|
||||
}
|
||||
return { type, path }
|
||||
return { type, path };
|
||||
}
|
||||
|
||||
export function typeForPath (path) {
|
||||
let type
|
||||
path = path.trim()
|
||||
export function typeForPath(path) {
|
||||
let type;
|
||||
path = path.trim();
|
||||
switch (true) {
|
||||
case /\.((png)|(jpg)|(jpeg))$/.test(path):
|
||||
type = 'Image'; break
|
||||
type = "Image";
|
||||
break;
|
||||
case /\.(mp4)$/.test(path):
|
||||
type = 'Video'; break
|
||||
type = "Video";
|
||||
break;
|
||||
case /\.(md)$/.test(path):
|
||||
type = 'Text'; break
|
||||
type = "Text";
|
||||
break;
|
||||
case /\.(pdf)$/.test(path):
|
||||
type = 'Document'; break
|
||||
type = "Document";
|
||||
break;
|
||||
default:
|
||||
type = 'Unknown'; break
|
||||
type = "Unknown";
|
||||
break;
|
||||
}
|
||||
return type
|
||||
return type;
|
||||
}
|
||||
|
||||
export function selectTypeFromPathWithPoster (path, poster) {
|
||||
return { type: typeForPath(path), path, poster }
|
||||
export function selectTypeFromPathWithPoster(path, poster) {
|
||||
return { type: typeForPath(path), path, poster };
|
||||
}
|
||||
|
||||
export function isIdentical (obj1, obj2) {
|
||||
return hash(obj1) === hash(obj2)
|
||||
export function isIdentical(obj1, obj2) {
|
||||
return hash(obj1) === hash(obj2);
|
||||
}
|
||||
|
||||
export function calcOpacity (num) {
|
||||
export function calcOpacity(num) {
|
||||
/* Events have opacity 0.5 by default, and get added to according to how many
|
||||
* other events there are in the same render. The idea here is that the
|
||||
* overlaying of events builds up a 'heat map' of the event space, where
|
||||
* darker areas represent more events with proportion */
|
||||
const base = num >= 1 ? 0.9 : 0
|
||||
return base + (Math.min(0.5, 0.08 * (num - 1)))
|
||||
const base = num >= 1 ? 0.9 : 0;
|
||||
return base + Math.min(0.5, 0.08 * (num - 1));
|
||||
}
|
||||
|
||||
export function calcClusterOpacity (pointCount, totalPoints) {
|
||||
export function calcClusterOpacity(pointCount, totalPoints) {
|
||||
/* Clusters represent multiple events within a specific radius. The darker the cluster,
|
||||
the larger the number of underlying events. We use a multiplication factor (50) here as well
|
||||
to ensure that the larger clusters have an appropriately darker shading. */
|
||||
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50)
|
||||
return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50);
|
||||
}
|
||||
|
||||
export function calcClusterSize (pointCount, totalPoints) {
|
||||
export function calcClusterSize(pointCount, totalPoints) {
|
||||
/* The larger the cluster size, the higher the count of points that the cluster represents.
|
||||
Just like with opacity, we use a multiplication factor to ensure that clusters with higher point
|
||||
counts appear larger. */
|
||||
const maxSize = totalPoints > 60 ? 40 : 20
|
||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150)
|
||||
const maxSize = totalPoints > 60 ? 40 : 20;
|
||||
return Math.min(maxSize, 10 + (pointCount / totalPoints) * 150);
|
||||
}
|
||||
|
||||
export function calculateTotalClusterPoints (clusters) {
|
||||
export function calculateTotalClusterPoints(clusters) {
|
||||
return clusters.reduce((total, cl) => {
|
||||
if (cl && cl.properties && cl.properties.cluster) {
|
||||
total += cl.properties.point_count
|
||||
total += cl.properties.point_count;
|
||||
}
|
||||
return total
|
||||
}, 0)
|
||||
return total;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function isLatitude (lat) {
|
||||
return !!lat && isFinite(lat) && Math.abs(lat) <= 90
|
||||
export function isLatitude(lat) {
|
||||
return !!lat && isFinite(lat) && Math.abs(lat) <= 90;
|
||||
}
|
||||
|
||||
export function isLongitude (lng) {
|
||||
return !!lng && isFinite(lng) && Math.abs(lng) <= 180
|
||||
export function isLongitude(lng) {
|
||||
return !!lng && isFinite(lng) && Math.abs(lng) <= 180;
|
||||
}
|
||||
|
||||
export function mapClustersToLocations (clusters, locations) {
|
||||
export function mapClustersToLocations(clusters, locations) {
|
||||
return clusters.reduce((acc, cl) => {
|
||||
const foundLocation = locations.find(location => location.label === cl.properties.id)
|
||||
if (foundLocation) acc.push(foundLocation)
|
||||
return acc
|
||||
}, [])
|
||||
const foundLocation = locations.find(
|
||||
(location) => location.label === cl.properties.id
|
||||
);
|
||||
if (foundLocation) acc.push(foundLocation);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through a set of either locations or events
|
||||
* and calculates the proportionate percentage of every given association in relation to the coloring set
|
||||
*/
|
||||
export function calculateColorPercentages (set, coloringSet) {
|
||||
if (coloringSet.length === 0) return [1]
|
||||
const associationMap = {}
|
||||
*/
|
||||
export function calculateColorPercentages(set, coloringSet) {
|
||||
if (coloringSet.length === 0) return [1];
|
||||
const associationMap = {};
|
||||
|
||||
for (const [idx, value] of coloringSet.entries()) {
|
||||
for (let filter of value) {
|
||||
associationMap[filter] = idx
|
||||
associationMap[filter] = idx;
|
||||
}
|
||||
}
|
||||
|
||||
const associationCounts = new Array(coloringSet.length)
|
||||
associationCounts.fill(0)
|
||||
const associationCounts = new Array(coloringSet.length);
|
||||
associationCounts.fill(0);
|
||||
|
||||
let totalAssociations = 0
|
||||
let totalAssociations = 0;
|
||||
|
||||
set.forEach(item => {
|
||||
let innerSet = 'events' in item ? item.events : item
|
||||
set.forEach((item) => {
|
||||
let innerSet = "events" in item ? item.events : item;
|
||||
|
||||
if (!Array.isArray(innerSet)) innerSet = [innerSet]
|
||||
if (!Array.isArray(innerSet)) innerSet = [innerSet];
|
||||
|
||||
innerSet.forEach(val => {
|
||||
val.associations.forEach(a => {
|
||||
const idx = associationMap[a]
|
||||
if (!idx && idx !== 0) return
|
||||
associationCounts[idx] += 1
|
||||
totalAssociations += 1
|
||||
})
|
||||
})
|
||||
})
|
||||
innerSet.forEach((val) => {
|
||||
val.associations.forEach((a) => {
|
||||
const idx = associationMap[a];
|
||||
if (!idx && idx !== 0) return;
|
||||
associationCounts[idx] += 1;
|
||||
totalAssociations += 1;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (totalAssociations === 0) return [1]
|
||||
if (totalAssociations === 0) return [1];
|
||||
|
||||
return associationCounts.map(count => count / totalAssociations)
|
||||
return associationCounts.map((count) => count / totalAssociations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,73 +375,76 @@ export function calculateColorPercentages (set, coloringSet) {
|
||||
*
|
||||
* Example coloringSet = [['Chemical', 'Tear Gas'], ['Procedural', 'Destruction of property']]
|
||||
*/
|
||||
export function getFilterIdxFromColorSet (filter, coloringSet) {
|
||||
let filterIdx = -1
|
||||
export function getFilterIdxFromColorSet(filter, coloringSet) {
|
||||
let filterIdx = -1;
|
||||
coloringSet.map((set, idx) => {
|
||||
const foundIdx = set.indexOf(filter)
|
||||
if (foundIdx !== -1) filterIdx = idx
|
||||
})
|
||||
return filterIdx
|
||||
const foundIdx = set.indexOf(filter);
|
||||
if (foundIdx !== -1) filterIdx = idx;
|
||||
});
|
||||
return filterIdx;
|
||||
}
|
||||
|
||||
export const dateMin = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a < b ? a : b
|
||||
})
|
||||
}
|
||||
return a < b ? a : b;
|
||||
});
|
||||
};
|
||||
|
||||
export const dateMax = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a > b ? a : b
|
||||
})
|
||||
}
|
||||
return a > b ? a : b;
|
||||
});
|
||||
};
|
||||
|
||||
/** Taken from
|
||||
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
||||
* **/
|
||||
export function binarySearch (ar, el, compareFn) {
|
||||
var m = 0
|
||||
var n = ar.length - 1
|
||||
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
||||
* **/
|
||||
export function binarySearch(ar, el, compareFn) {
|
||||
var m = 0;
|
||||
var n = ar.length - 1;
|
||||
while (m <= n) {
|
||||
var k = (n + m) >> 1
|
||||
var cmp = compareFn(el, ar[k])
|
||||
var k = (n + m) >> 1;
|
||||
var cmp = compareFn(el, ar[k]);
|
||||
if (cmp > 0) {
|
||||
m = k + 1
|
||||
m = k + 1;
|
||||
} else if (cmp < 0) {
|
||||
n = k - 1
|
||||
n = k - 1;
|
||||
} else {
|
||||
return k
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return -m - 1
|
||||
return -m - 1;
|
||||
}
|
||||
|
||||
export function makeNiceDate (datetime) {
|
||||
if (datetime === null) return null
|
||||
export function makeNiceDate(datetime) {
|
||||
if (datetime === null) return null;
|
||||
// see https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(
|
||||
language,
|
||||
{ year: 'numeric', month: 'long', day: '2-digit' }
|
||||
)
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "2-digit",
|
||||
});
|
||||
const [
|
||||
{ value: month },,
|
||||
{ value: day },,
|
||||
{ value: year }
|
||||
] = dateTimeFormat.formatToParts(datetime)
|
||||
{ value: month },
|
||||
,
|
||||
{ value: day },
|
||||
,
|
||||
{ value: year },
|
||||
] = dateTimeFormat.formatToParts(datetime);
|
||||
|
||||
return `${day} ${month}, ${year}`
|
||||
return `${day} ${month}, ${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default locale for d3 to format dates in each available language.
|
||||
* @param {Object} d3 - An instance of D3
|
||||
*/
|
||||
export function setD3Locale (d3) {
|
||||
export function setD3Locale(d3) {
|
||||
const languages = {
|
||||
'es-MX': require('./data/es-MX.json')
|
||||
}
|
||||
"es-MX": require("./data/es-MX.json"),
|
||||
};
|
||||
|
||||
if (language !== 'es-US' && languages[language]) {
|
||||
d3.timeFormatDefaultLocale(languages[language])
|
||||
if (language !== "es-US" && languages[language]) {
|
||||
d3.timeFormatDefaultLocale(languages[language]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as selectors from '../selectors'
|
||||
import { getFilterIdxFromColorSet } from '../common/utilities'
|
||||
import * as selectors from "../selectors";
|
||||
import { getFilterIdxFromColorSet } from "../common/utilities";
|
||||
// import Card from './Card.jsx'
|
||||
import { Card } from '@forensic-architecture/design-system/react'
|
||||
import copy from '../common/data/copy.json'
|
||||
import { Card } from "@forensic-architecture/design-system/react";
|
||||
import copy from "../common/data/copy.json";
|
||||
|
||||
class CardStack extends React.Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.refs = {}
|
||||
this.refCardStack = React.createRef()
|
||||
this.refCardStackContent = React.createRef()
|
||||
constructor() {
|
||||
super();
|
||||
this.refs = {};
|
||||
this.refCardStack = React.createRef();
|
||||
this.refCardStackContent = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const isNarrative = !!this.props.narrative
|
||||
componentDidUpdate() {
|
||||
const isNarrative = !!this.props.narrative;
|
||||
|
||||
if (isNarrative) {
|
||||
this.scrollToCard()
|
||||
this.scrollToCard();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToCard () {
|
||||
const duration = 500
|
||||
const element = this.refCardStack.current
|
||||
scrollToCard() {
|
||||
const duration = 500;
|
||||
const element = this.refCardStack.current;
|
||||
const cardScroll = this.refs[this.props.narrative.current].current
|
||||
.offsetTop
|
||||
.offsetTop;
|
||||
|
||||
let start = element.scrollTop
|
||||
let change = cardScroll - start
|
||||
let currentTime = 0
|
||||
const increment = 20
|
||||
let start = element.scrollTop;
|
||||
let change = cardScroll - start;
|
||||
let currentTime = 0;
|
||||
const increment = 20;
|
||||
|
||||
// t = current time
|
||||
// b = start value
|
||||
// c = change in value
|
||||
// d = duration
|
||||
Math.easeInOutQuad = function (t, b, c, d) {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t + b
|
||||
t -= 1
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||
}
|
||||
t /= d / 2;
|
||||
if (t < 1) return (c / 2) * t * t + b;
|
||||
t -= 1;
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b;
|
||||
};
|
||||
|
||||
const animateScroll = function () {
|
||||
currentTime += increment
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration)
|
||||
element.scrollTop = val
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment)
|
||||
}
|
||||
animateScroll()
|
||||
currentTime += increment;
|
||||
const val = Math.easeInOutQuad(currentTime, start, change, duration);
|
||||
element.scrollTop = val;
|
||||
if (currentTime < duration) setTimeout(animateScroll, increment);
|
||||
};
|
||||
animateScroll();
|
||||
}
|
||||
|
||||
renderCards (events, selections) {
|
||||
renderCards(events, selections) {
|
||||
// if no selections provided, select all
|
||||
if (!selections) {
|
||||
selections = events.map((e) => true)
|
||||
selections = events.map((e) => true);
|
||||
}
|
||||
this.refs = []
|
||||
this.refs = [];
|
||||
|
||||
return events.map((event, idx) => {
|
||||
const thisRef = React.createRef()
|
||||
this.refs[idx] = thisRef
|
||||
const thisRef = React.createRef();
|
||||
this.refs[idx] = thisRef;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -72,109 +72,109 @@ class CardStack extends React.Component {
|
||||
event,
|
||||
colors: this.props.colors,
|
||||
coloringSet: this.props.coloringSet,
|
||||
getFilterIdxFromColorSet
|
||||
getFilterIdxFromColorSet,
|
||||
})}
|
||||
language={this.props.language}
|
||||
isLoading={this.props.isLoading}
|
||||
isSelected={selections[idx]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderSelectedCards () {
|
||||
const { selected } = this.props
|
||||
renderSelectedCards() {
|
||||
const { selected } = this.props;
|
||||
|
||||
if (selected.length > 0) {
|
||||
return this.renderCards(selected)
|
||||
return this.renderCards(selected);
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
renderNarrativeCards () {
|
||||
const { narrative } = this.props
|
||||
const showing = narrative.steps
|
||||
renderNarrativeCards() {
|
||||
const { narrative } = this.props;
|
||||
const showing = narrative.steps;
|
||||
|
||||
const selections = showing.map((_, idx) => idx === narrative.current)
|
||||
const selections = showing.map((_, idx) => idx === narrative.current);
|
||||
|
||||
return this.renderCards(showing, selections)
|
||||
return this.renderCards(showing, selections);
|
||||
}
|
||||
|
||||
renderCardStackHeader () {
|
||||
const headerLang = copy[this.props.language].cardstack.header
|
||||
renderCardStackHeader() {
|
||||
const headerLang = copy[this.props.language].cardstack.header;
|
||||
|
||||
return (
|
||||
<div
|
||||
id='card-stack-header'
|
||||
className='card-stack-header'
|
||||
id="card-stack-header"
|
||||
className="card-stack-header"
|
||||
onClick={() => this.props.onToggleCardstack()}
|
||||
>
|
||||
<button className='side-menu-burg is-active'>
|
||||
<button className="side-menu-burg is-active">
|
||||
<span />
|
||||
</button>
|
||||
<p className='header-copy top'>
|
||||
<p className="header-copy top">
|
||||
{`${this.props.selected.length} ${headerLang}`}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderCardStackContent () {
|
||||
renderCardStackContent() {
|
||||
return (
|
||||
<div id='card-stack-content' className='card-stack-content'>
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>{this.renderSelectedCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderNarrativeContent () {
|
||||
renderNarrativeContent() {
|
||||
return (
|
||||
<div
|
||||
id='card-stack-content'
|
||||
className='card-stack-content'
|
||||
id="card-stack-content"
|
||||
className="card-stack-content"
|
||||
ref={this.refCardStackContent}
|
||||
>
|
||||
<ul>{this.renderNarrativeCards()}</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props
|
||||
render() {
|
||||
const { isCardstack, selected, narrative, timelineDims } = this.props;
|
||||
// TODO: make '237px', which is the narrative header, less hard-coded
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`
|
||||
const height = `calc(100% - 237px - ${timelineDims.height}px)`;
|
||||
if (selected.length > 0) {
|
||||
if (!narrative) {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
className={`card-stack
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
>
|
||||
{this.renderCardStackHeader()}
|
||||
{this.renderCardStackContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
id='card-stack'
|
||||
id="card-stack"
|
||||
ref={this.refCardStack}
|
||||
className={`card-stack narrative-mode
|
||||
${isCardstack ? '' : ' folded'}`}
|
||||
${isCardstack ? "" : " folded"}`}
|
||||
style={{ height }}
|
||||
>
|
||||
{this.renderNarrativeContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
narrative: selectors.selectActiveNarrative(state),
|
||||
selected: selectors.selectSelected(state),
|
||||
@@ -185,8 +185,8 @@ function mapStateToProps (state) {
|
||||
cardUI: state.ui.card,
|
||||
colors: state.ui.coloring.colors,
|
||||
coloringSet: state.app.associations.coloringSet,
|
||||
features: state.features
|
||||
}
|
||||
features: state.features,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CardStack)
|
||||
export default connect(mapStateToProps)(CardStack);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
import colors from '../../../common/global.js'
|
||||
import ColoredMarkers from './ColoredMarkers.jsx'
|
||||
import React, { useState } from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import colors from "../../../common/global.js";
|
||||
import ColoredMarkers from "./ColoredMarkers.jsx";
|
||||
import {
|
||||
calcClusterOpacity,
|
||||
calcClusterSize,
|
||||
@@ -9,18 +9,30 @@ import {
|
||||
isLongitude,
|
||||
calculateColorPercentages,
|
||||
zipColorsToPercentages,
|
||||
calculateTotalClusterPoints } from '../../../common/utilities'
|
||||
calculateTotalClusterPoints,
|
||||
} from "../../../common/utilities";
|
||||
|
||||
const DefsClusters = () => (
|
||||
<defs>
|
||||
<radialGradient id='clusterGradient'>
|
||||
<stop offset='10%' stop-color='red' />
|
||||
<stop offset='90%' stop-color='transparent' />
|
||||
<radialGradient id="clusterGradient">
|
||||
<stop offset="10%" stop-color="red" />
|
||||
<stop offset="90%" stop-color="transparent" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
)
|
||||
);
|
||||
|
||||
function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick, getClusterChildren, coloringSet, filterColors }) {
|
||||
function Cluster({
|
||||
cluster,
|
||||
size,
|
||||
projectPoint,
|
||||
totalPoints,
|
||||
styles,
|
||||
renderHover,
|
||||
onClick,
|
||||
getClusterChildren,
|
||||
coloringSet,
|
||||
filterColors,
|
||||
}) {
|
||||
/**
|
||||
{
|
||||
geometry: {
|
||||
@@ -35,22 +47,25 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
type: "Feature"
|
||||
}
|
||||
*/
|
||||
const { cluster_id: clusterId } = cluster.properties
|
||||
const { cluster_id: clusterId } = cluster.properties;
|
||||
|
||||
const individualChildren = getClusterChildren(clusterId)
|
||||
const colorPercentages = calculateColorPercentages(individualChildren, coloringSet)
|
||||
const individualChildren = getClusterChildren(clusterId);
|
||||
const colorPercentages = calculateColorPercentages(
|
||||
individualChildren,
|
||||
coloringSet
|
||||
);
|
||||
|
||||
const { coordinates } = cluster.geometry
|
||||
const [longitude, latitude] = coordinates
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null
|
||||
const { x, y } = projectPoint([latitude, longitude])
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const { coordinates } = cluster.geometry;
|
||||
const [longitude, latitude] = coordinates;
|
||||
const { x, y } = projectPoint([latitude, longitude]);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
if (!isLatitude(latitude) || !isLongitude(longitude)) return null;
|
||||
|
||||
return (
|
||||
<g
|
||||
className={'cluster-event'}
|
||||
className={"cluster-event"}
|
||||
transform={`translate(${x}, ${y})`}
|
||||
onClick={e => onClick({ id: clusterId, latitude, longitude })}
|
||||
onClick={(e) => onClick({ id: clusterId, latitude, longitude })}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
@@ -58,16 +73,16 @@ function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHove
|
||||
radius={size}
|
||||
colorPercentMap={zipColorsToPercentages(filterColors, colorPercentages)}
|
||||
styles={{
|
||||
...styles
|
||||
...styles,
|
||||
}}
|
||||
className={'cluster-event-marker'}
|
||||
className={"cluster-event-marker"}
|
||||
/>
|
||||
{hovered ? renderHover(cluster) : null}
|
||||
</g>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ClusterEvents ({
|
||||
function ClusterEvents({
|
||||
projectPoint,
|
||||
onSelect,
|
||||
getClusterChildren,
|
||||
@@ -76,56 +91,66 @@ function ClusterEvents ({
|
||||
svg,
|
||||
clusters,
|
||||
filterColors,
|
||||
selected
|
||||
selected,
|
||||
}) {
|
||||
const totalPoints = calculateTotalClusterPoints(clusters)
|
||||
const totalPoints = calculateTotalClusterPoints(clusters);
|
||||
|
||||
const styles = {
|
||||
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
|
||||
stroke: colors.darkBackground,
|
||||
strokeWidth: 0
|
||||
}
|
||||
strokeWidth: 0,
|
||||
};
|
||||
|
||||
function renderHover (txt, circleSize) {
|
||||
return <>
|
||||
<text text-anchor='middle' y='3px' style={{ fontWeight: 'bold', fill: 'black', zIndex: 10000 }}>{txt}</text>
|
||||
<circle
|
||||
class='event-hover'
|
||||
cx='0'
|
||||
cy='0'
|
||||
r={circleSize + 2}
|
||||
stroke={colors.primaryHighlight}
|
||||
fill-opacity='0.0'
|
||||
/>
|
||||
</>
|
||||
function renderHover(txt, circleSize) {
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
y="3px"
|
||||
style={{ fontWeight: "bold", fill: "black", zIndex: 10000 }}
|
||||
>
|
||||
{txt}
|
||||
</text>
|
||||
<circle
|
||||
class="event-hover"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r={circleSize + 2}
|
||||
stroke={colors.primaryHighlight}
|
||||
fill-opacity="0.0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal node={svg}>
|
||||
<g className='cluster-locations'>
|
||||
<g className="cluster-locations">
|
||||
{isRadial ? <DefsClusters /> : null}
|
||||
{clusters.map(c => {
|
||||
const pointCount = c.properties.point_count
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints)
|
||||
return <Cluster
|
||||
onClick={onSelect}
|
||||
getClusterChildren={getClusterChildren}
|
||||
coloringSet={coloringSet}
|
||||
cluster={c}
|
||||
filterColors={filterColors}
|
||||
size={clusterSize}
|
||||
projectPoint={projectPoint}
|
||||
totalPoints={totalPoints}
|
||||
styles={{
|
||||
...styles,
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
|
||||
}}
|
||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||
/>
|
||||
{clusters.map((c) => {
|
||||
const pointCount = c.properties.point_count;
|
||||
const clusterSize = calcClusterSize(pointCount, totalPoints);
|
||||
return (
|
||||
<Cluster
|
||||
onClick={onSelect}
|
||||
getClusterChildren={getClusterChildren}
|
||||
coloringSet={coloringSet}
|
||||
cluster={c}
|
||||
filterColors={filterColors}
|
||||
size={clusterSize}
|
||||
projectPoint={projectPoint}
|
||||
totalPoints={totalPoints}
|
||||
styles={{
|
||||
...styles,
|
||||
fillOpacity: calcClusterOpacity(pointCount, totalPoints),
|
||||
}}
|
||||
renderHover={() => renderHover(pointCount, clusterSize)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterEvents
|
||||
export default ClusterEvents;
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import store from './store/index.js'
|
||||
import App from './components/App.jsx'
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./store/index.js";
|
||||
import App from "./components/App.jsx";
|
||||
|
||||
console.log(process.env);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('explore-app')
|
||||
)
|
||||
document.getElementById("explore-app")
|
||||
);
|
||||
|
||||
// Expressions from https://exceptionshub.com/how-to-detect-safari-chrome-ie-firefox-and-opera-browser.html
|
||||
|
||||
/* eslint-disable */
|
||||
// Opera 8.0+
|
||||
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0
|
||||
const isOpera =
|
||||
(!!window.opr && !!opr.addons) ||
|
||||
!!window.opera ||
|
||||
navigator.userAgent.indexOf(" OPR/") >= 0;
|
||||
// Firefox 1.0+
|
||||
const isFirefox = typeof InstallTrigger !== 'undefined'
|
||||
const isFirefox = typeof InstallTrigger !== "undefined";
|
||||
// Safari 3.0+ "[object HTMLElementConstructor]"
|
||||
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === '[object SafariRemoteNotification]' })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification))
|
||||
const isSafari =
|
||||
/constructor/i.test(window.HTMLElement) ||
|
||||
(function (p) {
|
||||
return p.toString() === "[object SafariRemoteNotification]";
|
||||
})(
|
||||
!window["safari"] ||
|
||||
(typeof safari !== "undefined" && safari.pushNotification)
|
||||
);
|
||||
// Internet Explorer 6-11
|
||||
const isIE = /* @cc_on!@ */false || !!document.documentMode
|
||||
const isIE = /* @cc_on!@ */ false || !!document.documentMode;
|
||||
// Edge 20+
|
||||
const isEdge = !isIE && !!window.StyleMedia
|
||||
const isEdge = !isIE && !!window.StyleMedia;
|
||||
// Chrome 1+
|
||||
const isChrome = !!window.chrome && !!window.chrome.webstore
|
||||
const isChrome = !!window.chrome && !!window.chrome.webstore;
|
||||
// Blink engine detection
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS
|
||||
const isBlink = (isChrome || isOpera) && !!window.CSS;
|
||||
|
||||
if (isEdge || isIE) {
|
||||
alert('Please view this website in Opera for best viewing. It is untested in your browser.')
|
||||
alert(
|
||||
"Please view this website in Opera for best viewing. It is untested in your browser."
|
||||
);
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
Reference in New Issue
Block a user