/**
* Shows the machbanding form in the UI.
*/
function showMachBandingForm() {
const useMachbanding = document.getElementById('machbanding').checked;
document.getElementById('machbandingForm').style.display = (useMachbanding ? 'block' : 'none');
}
/**
* Shows the form corresponding to a tab referenced by its name.
* @param name the name of the tab to show.
*/
function openTab(name) {
let i;
let x = document.getElementsByClassName("tab");
for (i = 0; i < x.length; i++) {
x[i].style.display = "none";
}
document.getElementById(name).style.display = "block";
}
/**
* Changes the visibility of the height field in the UI.
* @param show a boolean flag. If true, this shows the field in the UI and hides it otherwise.
*/
function changeHeightInputVisibility(show = false) {
document.getElementById('stippleHeightContainer').style.display = show ? 'block' : 'none';
}
/**
* Shows the data set form corresponding to the user's data set selection in the UI.
*
* This also sets some default values for specific data set selections.
* E.g. the default width for image data sets is set to 200.
*/
function showDataSetForm() {
const dataSetForms = ['imageForm', 'gradientForm', 'customForm'];
const selected = document.forms['dataSetForm']['dataset'].value;
const selectedForm = `${selected}Form`;
dataSetForms.forEach(id => {
document.getElementById(id).style.display = (id === selectedForm ? 'block' : 'none');
})
const currentWidth = document.getElementById('stippleWidth').value;
switch (document.forms['dataSetForm']['dataset'].value) {
case 'gradient':
changeHeightInputVisibility(true);
let x2 = currentWidth;
if (parseInt(currentWidth) === 480) {
const newWidth = 300;
document.getElementById('stippleWidth').value = newWidth;
document.getElementById('stippleHeight').value = 150;
x2 = newWidth;
}
document.getElementById('gradientX2').value = x2;
break;
case 'custom':
changeHeightInputVisibility(true);
break;
case 'italy':
case 'eggholder':
case 'cglogo':
case 'meister':
case 'image':
if (parseInt(currentWidth) === 480) {
document.getElementById('stippleWidth').value = 200;
}
// fallthrough is intentional
default:
changeHeightInputVisibility(false);
break;
}
}
/**
* True if stippling is currently in progress, false otherwise.
* @type {boolean}
*/
let stipplingInProgress = false;
/**
* Holds the last stippled dataset.
* @type {null}
*/
let currentStippledDataSet = null;
/**
* Disables triggering the stippling process from the UI.
*/
function disableStippling() {
stipplingInProgress = true;
const stippleButton = document.getElementById('stippleButton');
stippleButton.style.backgroundColor = '#929292';
stippleButton.value = 'Stippling ...';
}
/**
* Enables triggering the stippling process from the UI.
*/
function enableStippling() {
stipplingInProgress = false;
const stippleButton = document.getElementById('stippleButton');
stippleButton.style.backgroundColor = '#4CAF50';
stippleButton.value = 'Stipple!';
}
/**
* Loads the topojson-features from a JSON file referenced by its name.
* @param name the name of the JSON file.
* @return {Promise<{inspector: boolean, debug: boolean, uv: boolean, ipv6: boolean, tls_alpn: boolean, tls_sni: boolean, tls_ocsp: boolean, tls: boolean}>} the laoded features
*/
async function loadStates(name) {
const states = await d3.json(name);
return topojson.feature(states, states.objects.states).features;
}
/**
* Creates a geoPath for a given resolution and projection method.
* @param width the target width of the projection
* @param height the target height of the projection
* @param projectionMethod the projection method to use when creating the geoPath. Defaults to 'geoAlbersUsa'
* @return {any} the created geoPath
*/
function geoPath(width, height, projectionMethod = 'geoAlbersUsa') {
const projection = createProjection(width, height, projectionMethod);
return d3.geoPath().projection(projection);
}
/**
* Calculates the scaling factor for a stipple based on the density it represents and a scaling method.
* @param density the density represented by a stipple
* @param stippleScaleMethod a scaling method
* @return {number} the scaling factor for the stipple.
*/
function getStippleScale(density, stippleScaleMethod) {
switch (stippleScaleMethod) {
case 'density':
return density;
case 'inverseDensity':
return 1 - density;
default:
return 1;
}
}
/**
* Calculates a color based on a density value (assumed to be in range [0,1]) and chosen color scale configuration.
*
* See also:
* - {@link colorScales}
* - {@link getColorString}
*
* @param density the density value used to calculate the color
* @param invertColors a boolean flag. If true 1-density is used instead of density.
* @param colorMap the name of a color map in {@link colorScales} or 'none'. If this is 'none' the resulting color will be either 'black' or 'white' depending on invertColors.
* @param interpolateColor a boolean flag. If true the colors between colors in the chosen color map are interpolated.
* @return {string} a color string which can be used as value for a 'fill' attribute.
*/
const getStippleColor = (density, invertColors, colorMap, interpolateColor) => {
if (invertColors) {
if (colorMap === 'none') {
return 'white';
} else {
return getColorString(1.0 - density, colorMap, interpolateColor);
}
} else {
if (colorMap === 'none') {
return 'black';
} else {
return getColorString(density, colorMap, interpolateColor);
}
}
}
/**
* Visualizes {@link currentStippledDataSet} if it is not null.
* Triggered on every change in the 'style' tab in the UI.
*
* If {@link stipplingInProgress} is true this function has no effect.
*
* @return {Promise<void>}
*/
async function visualizeCurrentStipples() {
// remove existing visualization
const visDiv = '#mapDiv';
d3.select(visDiv).select('svg').remove();
if (currentStippledDataSet && !stipplingInProgress) {
const outputScale = document.getElementById('visScale').value;
const scalingMethod = document.getElementById('stippleScale').value;
const colorMap = document.getElementById('stippleColorMap').value;
const interpolateColor = document.getElementById('interpolateColorMap').checked;
const invertColors = document.getElementById('invertColors').checked;
const matchBackground = document.getElementById('matchBackground').checked;
const outputWidth = currentStippledDataSet.width * outputScale;
const outputHeight = currentStippledDataSet.height * outputScale;
const svg = d3.select(visDiv)
.append('svg')
.attr('width', outputWidth)
.attr('height', outputHeight);
if ((invertColors && matchBackground) || (!invertColors && !matchBackground)) {
svg.append('rect')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', 'black');
}
if (currentStippledDataSet.geographicalDataset) {
const path = geoPath(outputWidth, outputHeight);
svg.append('g')
.attr('id', 'states')
.selectAll('path')
.data(await loadStates("county_us.topojson"))
.enter().append('path')
.attr('d', path)
//.attr('class', 'state')
.style('fill', 'none')
.style('stroke', 'grey')
.style('stroke-width', '1px');
}
const card = initCard(currentStippledDataSet, currentStippledDataSet.geographicalDataset)
currentStippledDataSet.stipples.forEach(s => {
if (s.density !== 0.0) {
svg.append('circle')
.attr('cx', s.relativeX * outputWidth)
.attr('cy', s.relativeY * outputHeight)
.attr('r', currentStippledDataSet.stippleRadius * outputScale * getStippleScale(s.density, scalingMethod))
.style('fill', getStippleColor(s.density, invertColors, colorMap, interpolateColor))
.on('mouseenter', function () {
d3.select(this).style('fill', 'rgb(255, 0, 0)'); // color stipples
if (currentStippledDataSet.geographicalDataset) {
let bounds = stippleBounds(s, currentStippledDataSet.voronoi);
card.drawArea(bounds, currentStippledDataSet.voronoi, getVoronoiCell(s.position(), currentStippledDataSet.voronoi));
}
})
.on('mouseleave', function () {
d3.select(this).style('fill', getStippleColor(s.density, invertColors, colorMap, interpolateColor));
})
}
})
}
}
/**
* Creates a {@link Card} filled with a given stippled data set.
* @param data a stippled data set (e.g. {@link currentStippledDataSet}
* @param geographical a boolean flag. If true, the created {@link Card} is displayed, otherwise it's hidden.
* @return {Card} the created {@link Card}.
*/
const initCard = (data, geographical) => {
const cardDiv = '#cardDiv';
d3.select(cardDiv).select('svg').remove();
const box = document.querySelector(cardDiv);
const detailsDiv = 'details'
const details = document.getElementById(detailsDiv);
if (geographical) {
details.style.display = 'block';
} else {
details.style.display = 'none';
}
const myData = {data: data.locationToData, width: data.width, height: data.height};
return new Card(myData, box.clientWidth, box.clientHeight, cardDiv);
}
/**
* Collects all data from the UI and stipples the chosen data set using the chosen parameters.
* As soon as it has completed {@link currentStippledDataSet} is overwritten and {@link visualizeCurrentStipples} is
* triggered.
*
* If {@link stipplingInProgress} is true this function has no effect.
*
* See also:
* - {@link stipple}
* - {@link DensityFunction2D#fromImageData2D}
* - {@link DensityFunction2D#machBandingFromImageData2D}
* - {@link createGeographicDataImage}
* - {@link createImageData}
* - {@link createGradientImage}
* - {@link createImageFromData}
*
* @return {boolean} always false.
*/
function stippleDataSet() {
if (!stipplingInProgress) {
disableStippling();
const width = parseInt(document.getElementById('stippleWidth').value);
const height = parseInt(document.getElementById('stippleHeight').value);
const stippleRadius = parseFloat(document.getElementById('stippleRadius').value);
const useMachbanding = document.getElementById('machbanding').checked;
const machbandingQuantization = parseInt(document.getElementById('machbandingQuantization').value);
const machbandingWeight = parseFloat(document.getElementById('machbandingWeight').value);
const machbandingBlurRadius = parseFloat(document.getElementById('machbandingBlurRadius').value);
const dataset = document.forms['dataSetForm']['dataset'].value;
let dataSourceFunc;
let geographicalDataset;
switch (dataset) {
case 'accidentsNov':
dataSourceFunc = async () => {
const data = await d3.csv("us-accidents-severity-4-Nov-Dec-2020.csv");
return createGeographicDataImage(data, width);
};
geographicalDataset = true;
break;
case 'accidentsDec':
dataSourceFunc = async () => {
const data = await d3.csv("us-accidents-severity-4-Dec-2020.csv");
return createGeographicDataImage(data, width);
};
geographicalDataset = true;
break;
case 'hailstorm0':
dataSourceFunc = async () => {
const data = await d3.csv("hail-2015-sevprob-larger-0.csv");
return createGeographicDataImage(data, width);
};
geographicalDataset = true;
break;
case 'hailstorm56':
dataSourceFunc = async () => {
const data = await d3.csv("hail-2015-sevprob-larger-56.csv");
return createGeographicDataImage(data, width);
};
geographicalDataset = true;
break;
case 'hailstorm80':
dataSourceFunc = async () => {
const data = await d3.csv("hail-2015-sevprob-larger-80.csv");
return createGeographicDataImage(data, width);
};
geographicalDataset = true;
break;
case 'italy':
geographicalDataset = false;
dataSourceFunc = async () => {
return createImageData('italy.png', width);
}
break;
case 'eggholder':
geographicalDataset = false;
dataSourceFunc = async () => {
return createImageData('eggholder.png', width);
}
break;
case 'cglogo':
geographicalDataset = false;
dataSourceFunc = async () => {
return createImageData('E193-02.png', width);
}
break;
case 'meister':
geographicalDataset = false;
dataSourceFunc = async () => {
return createImageData('Meister.jpg', width);
}
break;
case 'gradient':
const fromTo = [
document.getElementById('gradientX1').value,
document.getElementById('gradientY1').value,
document.getElementById('gradientX2').value,
document.getElementById('gradientY2').value,
];
dataSourceFunc = async () => {
const gradient = createGradientImage(width, height, fromTo);
return {
densityImage: gradient,
locationToData: gradient
};
};
geographicalDataset = false;
break;
case 'image':
const imageFile = document.getElementById('imageToStipple').files[0];
geographicalDataset = false;
dataSourceFunc = async () => {
return createImageData(await readFile(imageFile), width);
}
break;
case 'custom':
const dataSource = document.getElementById('customDataURL').value;
const projection = document.getElementById('customProjection').value;
const xAttribute = document.getElementById('xAttribute').value;
const yAttribute = document.getElementById('yAttribute').value;
const scaleByMaxDensity = document.getElementById('useGrayscale').checked;
if (projection === 'none') {
geographicalDataset = false;
}
dataSourceFunc = async () => {
let data;
if (dataSource.toLowerCase().endsWith('.csv')) {
data = await d3.csv(dataSource);
} else if (dataSource.toLowerCase().endsWith('.json')) {
data = await d3.json(dataSource);
}
if (projection === 'none') {
return createImageFromData(data, width, height, xAttribute, yAttribute, null, scaleByMaxDensity);
} else {
return createGeographicDataImage(data, width, height, projection, xAttribute, yAttribute, scaleByMaxDensity);
}
};
break;
}
(async () => {
const {densityImage, locationToData} = await dataSourceFunc();
let densityFunction;
if (useMachbanding) {
densityFunction = DensityFunction2D.machBandingFromImageData2D(
densityImage, machbandingQuantization, machbandingWeight, machbandingBlurRadius, rgbaToLuminance, null);
} else {
densityFunction = DensityFunction2D.fromImageData2D(densityImage);
}
const {stipples, voronoi} = await stipple(densityFunction, stippleRadius);
return {
width: densityFunction.width,
height: densityFunction.height,
stippleRadius,
datasetType: dataset,
densityImage,
locationToData,
stipples,
voronoi,
geographicalDataset
};
})().then(stippledDataset => {
currentStippledDataSet = stippledDataset;
enableStippling();
visualizeCurrentStipples().then(r => {
console.log("Done with stippling");
});
}).catch(console.error);
}
return false; // i.e. do not refresh the page
}