Skip to main content

Time-of-day lighting based on the map point

Lighting in a map style (light) is a global parameter: its sources are resolved once per frame and do not depend on individual objects. So a "sun above a point" is defined not by an object's position, but by the time of day that you compute in a style expression. By changing this time as the map moves, you can get a realistic picture: daytime in Moscow and evening in the east, with matching shadow directions.

This guide shows how to build such lighting from two global style variables:

  • timeOffsetMinutes — the time zone offset from UTC (in minutes eastwards) for the current map point;
  • dynamicLightingEnabled — a flag that turns dynamic lighting on.
note

Lighting is a per-frame global parameter, so you cannot render different lighting for different points of the screen at the same time: the light corresponds to the center of the map. When the map moves, the whole frame switches to the local time of the new point.

How it works​

The minutes of the day at a point are computed by combining three expressions:

  • ["time", ["utc-time"], "day-minutes"] — the current minutes from midnight in UTC (range 0…1439). Note that utc-time must be wrapped in time(...) — without the wrapper the expression returns a timestamp in milliseconds instead of minutes of the day.
  • ["global", "timeOffsetMinutes"] — the time zone offset of the point, passed from the code.

The resulting time key:

["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"]

The sign of the offset is minutes east of UTC: Moscow (UTC+3) → 180, Yekaterinburg (UTC+5) → 300, Yakutsk (UTC+9) → 540.

Configuring the style​

All lighting expressions are defined in the light object of the style. Below is an example for the sun source (directional, also responsible for shadows) and atmosphere (ambient). Use these fragments as a starting point in the Style editor.

The color and intensity of the sun change over the time of day, while azimuth and altitude define the sun position that drives shadow direction:

{
"sun": {
"type": "directional",
"color": ["interpolate", ["linear"], ["zoom"],
16, "#FFFFFF",
17,
["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, "#8FA6D8",
360, "#FFB27A",
720, "#FFFFFF",
1080, "#FF9E6E",
1440, "#8FA6D8"
],
"#FFFFFF"
]
],
"azimuth": ["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, 0,
360, 90,
720, 180,
1080, 270,
1440, 360
],
230
],
"altitude": ["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, 5,
720, 50,
1440, 5
],
50
]
},
"atmosphere": {
"type": "ambient",
"color": ["interpolate", ["linear"], ["zoom"],
16, "#FFFFFF",
17,
["when", ["global", "dynamicLightingEnabled"],
["interpolate", ["linear"],
["time", ["utc-time", ["global", "timeOffsetMinutes"]], "day-minutes"],
0, "#AFC0DE",
360, "#FFD9C0",
720, "#FFFFFF",
1080, "#FFCBB0",
1440, "#AFC0DE"
],
"#FFFFFF"
]
]
}
}

Explanation of the construction:

  • interpolate over ["zoom"] preserves the current behavior at small zooms and enables dynamics only at a large scale (17 and above). If needed, remove the outer interpolate and keep only the time branch.
  • when chooses between the dynamic value (flag true) and the static value (flag false or the global variable is not set). when works both as a wrapper over the entire zoom branch and over a single property.
  • azimuth and altitude — the sun position over time. Azimuth is measured from north: 90 — east (sunrise), 180 — south (noon), 270 — west (sunset). These values define the shadow direction.
  • Shadows are enabled by binding the source to shadows in the shadows section ("shadows": { "source": "sun" }) — then the shadow direction follows azimuth/altitude.

Computing the time offset in code​

The timeOffsetMinutes value must be computed for the map center and passed to the style via the map.patchStyleState() method. Recompute it when the map stops moving (moveend) and once a minute — in case of a day change or daylight saving time transition.

Approximate, by longitude​

The simplest option without dependencies: the time zone is estimated from the longitude (15° = 1 hour). It ignores daylight saving time and offset zones (China, India), but this is usually enough for a visual effect.

function offsetMinutesForLng(lng) {
return Math.round(lng / 15) * 60;
}

function updatePointTimeOffset(map) {
const [lng] = map.getCenter();
map.patchStyleState({ timeOffsetMinutes: offsetMinutesForLng(lng) });
}

Precise, by IANA zone​

If political zone borders and daylight saving time matter, determine the IANA zone by coordinates (for example, with the tz-lookup package) and get the offset via Intl:

function zoneOffsetMinutes(timeZone, date = new Date()) {
const name = new Intl.DateTimeFormat('en-US', { timeZone, timeZoneName: 'longOffset' })
.formatToParts(date)
.find((part) => part.type === 'timeZoneName').value;
const match = /GMT([+-])(\d{2}):(\d{2})/.exec(name);
return match ? (match[1] === '-' ? -1 : 1) * (+match[2] * 60 + +match[3]) : 0;
}

function updatePointTimeOffset(map) {
const [lng, lat] = map.getCenter();
const timeZone = tzlookup(lat, lng);
map.patchStyleState({ timeOffsetMinutes: zoneOffsetMinutes(timeZone) });
}

Initialization and subscriptions​

function enableDynamicLighting(map) {
let lastOffset = NaN;

const refresh = () => {
const [lng] = map.getCenter();
const offset = offsetMinutesForLng(lng);
if (offset === lastOffset) {
return;
}
lastOffset = offset;
map.patchStyleState({ timeOffsetMinutes: offset });
};

document.querySelector('#lighting-toggle').addEventListener('change', (event) => {
map.patchStyleState({ dynamicLightingEnabled: event.target.checked });
});

map.on('moveend', refresh);
setInterval(refresh, 60 * 1000);
refresh();
}

enableDynamicLighting(map);

Usage example​

In the example, the checkbox turns dynamic lighting on and off via the dynamicLightingEnabled global variable. The lighting itself must be configured in the style beforehand (see the Configuring the style section). As you move the map, you will see the light and shadow direction change.

Limitations​

  • One light per frame. Lighting is a global parameter, so "daytime in Moscow and evening in Yakutsk" cannot be shown on a single screen at the same time. The light always corresponds to the map center.
  • Time zone accuracy. Approximation by longitude (lng / 15) ignores zone borders and daylight saving time. For an accurate result, use the IANA zone and Intl (see above).