MediaWiki:Gadget-maps.js: Difference between revisions
Jump to navigation
Jump to search
Minh Nguyen (talk | contribs) mNo edit summary |
Minh Nguyen (talk | contribs) (Fixed conversion of negative and far-future dates to decimal years) |
||
Line 209: | Line 209: | ||
*/ |
*/ |
||
function decimalYearFromISODate(isoDate) { |
function decimalYearFromISODate(isoDate) { |
||
// Require a valid YYYY, YYYY-MM, or YYYY-MM-DD date, but allow the year |
|||
if (!isoDate) return; |
|||
// to be a variable number of digits or negative, unlike ISO 8601-1. |
|||
⚫ | |||
if (!isoDate || !/^-?\d{1,4}(?:-\d\d){0,2}$/.test(isoDate)) return; |
|||
var ymd = isoDate.split("-"); |
|||
// A negative year results in an extra element at the beginning. |
|||
if (ymd[0] === "") { |
|||
ymd.shift(); |
|||
ymd[0] *= -1; |
|||
} |
|||
var year = +ymd[0]; |
|||
var date = dateFromUTC(year, +ymd[1] - 1, +ymd[2]); |
|||
if (isNaN(date)) return; |
if (isNaN(date)) return; |
||
// Add the year and the fraction of the date between two New Year’s Days. |
// Add the year and the fraction of the date between two New Year’s Days. |
||
var |
var nextNewYear = dateFromUTC(year + 1, 0, 1).getTime(); |
||
var |
var lastNewYear = dateFromUTC(year, 0, 1).getTime(); |
||
var lastNewYear = new Date(year + "-01-01").getTime(); |
|||
return year + (date.getTime() - lastNewYear) / (nextNewYear - lastNewYear); |
return year + (date.getTime() - lastNewYear) / (nextNewYear - lastNewYear); |
||
} |
|||
/** |
|||
* Returns a `Date` object representing the given UTC date components. |
|||
* |
|||
* @param year A one-based year in the proleptic Gregorian calendar. |
|||
* @param month A zero-based month. |
|||
* @param day A one-based day. |
|||
* @returns A date object. |
|||
*/ |
|||
function dateFromUTC(year, month, day) { |
|||
⚫ | |||
// Date.UTC() treats a two-digit year as an offset from 1900. |
|||
date.setUTCFullYear(year); |
|||
return date; |
|||
} |
} |
||
Revision as of 22:46, 5 June 2023
/**
* Embeds a MapLibre GL map into any wiki page that asks for one.
*
* To embed a map, add a <div> with the class `maplibre-map`. Use data
* attributes to specify the map’s parameters:
*
* - `data-width`: Width of the map. `full` fits available space.
* - `data-height`: Height of the map.
* - `data-layer`: Vector style or raster tileset ID; see configuration below.
* - `data-lat`: Initial center latitude.
* - `data-lon`: Initial center longitude.
* - `data-zoom`: Initial vector zoom level (one less than raster).
* - `data-bearing`: Initial bearing in degrees counterclockwise from north.
* - `data-pitch`: Initial pitch in degrees away from the plane of the screen.
* - `data-date`: ISO 8601-1 date (for OpenHistoricalMap layers).
* - `data-mlat`: Marker latitude.
* - `data-mlon`: Marker longitude.
*
* To embed a scrubber that compares two maps, wrap the two `maplibre-map`
* <div>s in a <div> with the class `maplibre-comparison`. Use data attributes
* to specify the comparison’s parameters:
*
* - `data-width`: Width of the comparison. `full` fits available space.
* - `data-height`: Height of the comparison.
*/
$(function () {
var containers = $(".maplibre-map");
if (containers.length === 0) return;
mw.loader.load(mw.util.getUrl("MediaWiki:Gadget-maplibre.css", { action: "raw", ctype: "text/css" }), "text/css");
mw.loader.getScript(mw.util.getUrl("MediaWiki:Gadget-maplibre.js", { action: "raw", ctype: "text/javascript" })).then(function () {
containers.each(function () {
populateContainer(this);
});
var comparisons = $(".maplibre-comparison");
if (comparisons.length === 0) return;
mw.loader.load(mw.util.getUrl("MediaWiki:Gadget-maplibre-gl-compare.css", { action: "raw", ctype: "text/css" }), "text/css");
mw.loader.getScript(mw.util.getUrl("MediaWiki:Gadget-maplibre-gl-compare.js", { action: "raw", ctype: "text/javascript" })).then(function () {
comparisons.each(function () {
populateComparison(this);
});
});
});
/**
* Populates an HTML element with an interactive map.
*
* @param container The HTML element to populate.
*/
function populateContainer(container) {
// Configuration
var osmAttribution = "Map data © <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap contributors</a>";
var ohmAttribution = "<a href='https://www.openhistoricalmap.org/copyright'>OpenHistoricalMap contributors</a>";
var layers = {
americana: {
style: "https://zelonewolf.github.io/openstreetmap-americana/style.json",
},
baremaps: {
style: "https://demo.baremaps.com/style.json",
},
carto: {
tileset: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
name: "OpenStreetMap Carto",
attribution: osmAttribution,
},
"cycle-map": {
tileset: "https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=6170aad10dfd42a38d4d8c709a536f38",
name: "OpenCycleMap",
attribution: osmAttribution + ", map style <a href='https://www.thunderforest.com/'>Thunderforest</a>",
},
cyclosm: {
tileset: "https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png",
name: "CyclOSM",
attribution: osmAttribution + ", map style <a href='https://github.com/cyclosm/cyclosm-cartocss-style/releases'>CyclOSM v0.6</a>",
},
historic: {
style: "https://openhistoricalmap.github.io/map-styles/ohm_timeslider_tegola/tegola-ohm.json",
afterLoad: [filterByDate],
attribution: ohmAttribution,
},
humanitarian: {
tileset: "https://tile-a.openstreetmap.fr/hot/{z}/{x}/{y}.png",
name: "Humanitarian",
attribution: osmAttribution + ", map style <a href='https://hotosm.org/'>Humanitarian OpenStreetMap Team</a>, hosted by <a href='https://openstreetmap.fr/'>OpenStreetMap France</a>",
},
"japanese scroll": {
style: "https://openhistoricalmap.github.io/map-styles/japanese_scroll/ohm-japanese-scroll-map.json",
afterLoad: [filterByDate],
attribution: ohmAttribution,
},
oepnv: {
tileset: "https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png",
name: "ÖPNVKarte",
attribution: osmAttribution + ", tiles courtesy of <a href='https://memomaps.de/'>MeMoMaps</a>",
},
"transport-map": {
tileset: "https://tile.thunderforest.com/transport/{z}/{x}/{y}.png?apikey=6170aad10dfd42a38d4d8c709a536f38",
name: "Transport",
attribution: osmAttribution + ", map style <a href='https://www.thunderforest.com/'>Thunderforest</a>",
maxZoom: 17,
},
woodblock: {
style: "https://openhistoricalmap.github.io/map-styles/woodblock/woodblock.json",
afterLoad: [filterByDate],
attribution: ohmAttribution,
},
};
var width = $(container).data("width");
if (width === "full") {
width = "100%";
}
$(container)
.css("width", width)
.css("height", $(container).data("height"));
var layerID = $(container).data("layer");
var layer = layers[layerID] || layers.baremaps;
var style = layer.style || wrapTileset(layer);
var mapOptions = {
container: container,
style: style,
center: [$(container).data("lon") || 0, $(container).data("lat") || 0],
zoom: $(container).data("zoom") || 0,
bearing: $(container).data("bearing") || 0,
pitch: $(container).data("pitch") || 0,
cooperativeGestures: true,
};
// Some vector styles need attribution to be overwritten.
if (layer.attribution && !layer.tileset) {
mapOptions.customAttribution = layer.attribution;
}
var map = new maplibregl.Map(mapOptions);
container.map = map;
var markerLatitude = $(container).data("mlat");
var markerLongitude = $(container).data("mlon");
if (markerLatitude || markerLongitude) {
new maplibregl.Marker()
.setLngLat([markerLongitude, markerLatitude])
.addTo(map);
}
// Call any runtime styling functions that need to run after the layer loads.
map.on("load", function () {
var fns = layer.afterLoad || [];
fns.forEach(function (fn) {
fn(container);
});
});
}
function wrapTileset(layer) {
var style = {
version: 8,
name: layer.name,
sources: {},
layers: [{
id: "raster",
type: "raster",
source: "raster",
}],
};
style.sources.raster = {
type: "raster",
tiles: [layer.tileset],
tileSize: 256,
};
if (typeof(layer.attribution) === "string") {
style.sources.raster.attribution = layer.attribution;
}
if ("maxZoom" in layer) {
// Raster zoom levels are one more than vector zoom levels.
style.sources.raster.maxzoom = layer.maxZoom - 1;
}
return style;
}
/**
* Filters the map’s features by the `date` data attribute.
*
* @param container The HTML element containing the map.
*/
function filterByDate(container) {
var date = $(container).data("date");
var decimalYear = date && decimalYearFromISODate(date);
if (!decimalYear) return;
var map = container.map;
map.getStyle().layers.map(function (layer) {
if (!("source-layer" in layer)) return;
var filter = constrainFilterByDate(layer.filter, decimalYear);
map.setFilter(layer.id, filter);
});
}
/**
* Converts the given ISO 8601-1 date to a decimal year.
*
* @param isoDate A date string in ISO 8601-1 format.
* @returns A floating point number of years since year 0.
*/
function decimalYearFromISODate(isoDate) {
// Require a valid YYYY, YYYY-MM, or YYYY-MM-DD date, but allow the year
// to be a variable number of digits or negative, unlike ISO 8601-1.
if (!isoDate || !/^-?\d{1,4}(?:-\d\d){0,2}$/.test(isoDate)) return;
var ymd = isoDate.split("-");
// A negative year results in an extra element at the beginning.
if (ymd[0] === "") {
ymd.shift();
ymd[0] *= -1;
}
var year = +ymd[0];
var date = dateFromUTC(year, +ymd[1] - 1, +ymd[2]);
if (isNaN(date)) return;
// Add the year and the fraction of the date between two New Year’s Days.
var nextNewYear = dateFromUTC(year + 1, 0, 1).getTime();
var lastNewYear = dateFromUTC(year, 0, 1).getTime();
return year + (date.getTime() - lastNewYear) / (nextNewYear - lastNewYear);
}
/**
* Returns a `Date` object representing the given UTC date components.
*
* @param year A one-based year in the proleptic Gregorian calendar.
* @param month A zero-based month.
* @param day A one-based day.
* @returns A date object.
*/
function dateFromUTC(year, month, day) {
var date = new Date(Date.UTC(year, month, day));
// Date.UTC() treats a two-digit year as an offset from 1900.
date.setUTCFullYear(year);
return date;
}
/**
* Returns a modified version of the given filter that only evaluates to
* true if the feature coincides with the given decimal year.
*
* @param filter The original layer filter.
* @param decimalYear The decimal year to filter by.
* @returns A filter similar to the given filter, but with added conditions
* that require the feature to coincide with the decimal year.
*/
function constrainFilterByDate(filter, decimalYear) {
var dateFilter = [
"all",
["any", ["!has", "start_decdate"], ["<=", "start_decdate", decimalYear]],
["any", ["!has", "end_decdate"], [">=", "end_decdate", decimalYear]],
];
if (filter) {
dateFilter.push(filter);
}
return dateFilter;
}
/**
* Populates an HTML element with an interactive comparison between two
* maps.
*
* @param container The HTML element to populate.
*/
function populateComparison(comparison) {
var width = $(comparison).data("width");
if (width === "full") {
width = "100%";
}
$(comparison).css({
position: "relative",
width: width,
height: $(comparison).data("height"),
});
var containers = $(comparison).find(".maplibre-map");
containers.css({
position: "absolute",
top: 0,
bottom: 0,
width: "100%",
});
comparison.compare = new maplibregl.Compare(containers[0].map, containers[1].map, comparison);
}
});