diff --git a/package-lock.json b/package-lock.json index 96efce21..d65d2b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -629,6 +629,10 @@ "resolved": "samples/ui-kit-place-search-text", "link": true }, + "node_modules/@js-api-samples/weather-api-compact": { + "resolved": "samples/weather-api-compact", + "link": true + }, "node_modules/@loaders.gl/core": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.3.tgz", @@ -1113,8 +1117,7 @@ "version": "3.58.1", "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/node": { "version": "22.15.29", @@ -2480,6 +2483,9 @@ "samples/ui-kit-place-search-text": { "name": "@js-api-samples/ui-kit-place-search-text", "version": "1.0.0" + }, + "samples/weather-api-compact": { + "version": "1.0.0" } } } diff --git a/samples/weather-api-current-compact/README.md b/samples/weather-api-current-compact/README.md new file mode 100644 index 00000000..be15c0d7 --- /dev/null +++ b/samples/weather-api-current-compact/README.md @@ -0,0 +1,32 @@ +# Google Maps JavaScript Sample + +This sample is generated from @googlemaps/js-samples located at +https://github.com/googlemaps-samples/js-api-samples. + +## Setup + +### Before starting run: + +`$npm i` + +### Run an example on a local web server + +First `cd` to the folder for the sample to run, then: + +`$npm start` + +### Build an individual example + +From `samples/`: + +`$npm run build --workspace=sample-name/` + +### Build all of the examples. + +From `samples/`: +`$npm run build-all` + +## Feedback + +For feedback related to this sample, please open a new issue on +[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues). diff --git a/samples/weather-api-current-compact/icons/air-pressure.svg b/samples/weather-api-current-compact/icons/air-pressure.svg new file mode 100644 index 00000000..57ccf401 --- /dev/null +++ b/samples/weather-api-current-compact/icons/air-pressure.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/cloud-cover-white.svg b/samples/weather-api-current-compact/icons/cloud-cover-white.svg new file mode 100644 index 00000000..a79502c8 --- /dev/null +++ b/samples/weather-api-current-compact/icons/cloud-cover-white.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/cloud-cover.svg b/samples/weather-api-current-compact/icons/cloud-cover.svg new file mode 100644 index 00000000..e1011733 --- /dev/null +++ b/samples/weather-api-current-compact/icons/cloud-cover.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/heat-index-white.svg b/samples/weather-api-current-compact/icons/heat-index-white.svg new file mode 100644 index 00000000..ef2ec549 --- /dev/null +++ b/samples/weather-api-current-compact/icons/heat-index-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/heat-index.svg b/samples/weather-api-current-compact/icons/heat-index.svg new file mode 100644 index 00000000..3e1bef98 --- /dev/null +++ b/samples/weather-api-current-compact/icons/heat-index.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/ice-thickness.svg b/samples/weather-api-current-compact/icons/ice-thickness.svg new file mode 100644 index 00000000..083e591e --- /dev/null +++ b/samples/weather-api-current-compact/icons/ice-thickness.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/rain-probability-white.svg b/samples/weather-api-current-compact/icons/rain-probability-white.svg new file mode 100644 index 00000000..372be77b --- /dev/null +++ b/samples/weather-api-current-compact/icons/rain-probability-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/rain-probability.svg b/samples/weather-api-current-compact/icons/rain-probability.svg new file mode 100644 index 00000000..361b8527 --- /dev/null +++ b/samples/weather-api-current-compact/icons/rain-probability.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/relative-humidity-white.svg b/samples/weather-api-current-compact/icons/relative-humidity-white.svg new file mode 100644 index 00000000..be03aba8 --- /dev/null +++ b/samples/weather-api-current-compact/icons/relative-humidity-white.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/relative-humidity.svg b/samples/weather-api-current-compact/icons/relative-humidity.svg new file mode 100644 index 00000000..ca228cb1 --- /dev/null +++ b/samples/weather-api-current-compact/icons/relative-humidity.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/sunrise.svg b/samples/weather-api-current-compact/icons/sunrise.svg new file mode 100644 index 00000000..922e21d3 --- /dev/null +++ b/samples/weather-api-current-compact/icons/sunrise.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/sunset.svg b/samples/weather-api-current-compact/icons/sunset.svg new file mode 100644 index 00000000..538a5f61 --- /dev/null +++ b/samples/weather-api-current-compact/icons/sunset.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/thunderstorm-white.svg b/samples/weather-api-current-compact/icons/thunderstorm-white.svg new file mode 100644 index 00000000..dd5a4a89 --- /dev/null +++ b/samples/weather-api-current-compact/icons/thunderstorm-white.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/thunderstorm.svg b/samples/weather-api-current-compact/icons/thunderstorm.svg new file mode 100644 index 00000000..717d9ce1 --- /dev/null +++ b/samples/weather-api-current-compact/icons/thunderstorm.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/uv-index-white.svg b/samples/weather-api-current-compact/icons/uv-index-white.svg new file mode 100644 index 00000000..dc12a530 --- /dev/null +++ b/samples/weather-api-current-compact/icons/uv-index-white.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/uv-index.svg b/samples/weather-api-current-compact/icons/uv-index.svg new file mode 100644 index 00000000..ac37fb3f --- /dev/null +++ b/samples/weather-api-current-compact/icons/uv-index.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/visibility.svg b/samples/weather-api-current-compact/icons/visibility.svg new file mode 100644 index 00000000..1b3e7d5e --- /dev/null +++ b/samples/weather-api-current-compact/icons/visibility.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/wind-arrow.svg b/samples/weather-api-current-compact/icons/wind-arrow.svg new file mode 100644 index 00000000..a4fd6d88 --- /dev/null +++ b/samples/weather-api-current-compact/icons/wind-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/wind-chill.svg b/samples/weather-api-current-compact/icons/wind-chill.svg new file mode 100644 index 00000000..3678c889 --- /dev/null +++ b/samples/weather-api-current-compact/icons/wind-chill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/icons/wind.svg b/samples/weather-api-current-compact/icons/wind.svg new file mode 100644 index 00000000..3678c889 --- /dev/null +++ b/samples/weather-api-current-compact/icons/wind.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/weather-api-current-compact/index.html b/samples/weather-api-current-compact/index.html new file mode 100644 index 00000000..1a6e662f --- /dev/null +++ b/samples/weather-api-current-compact/index.html @@ -0,0 +1,33 @@ + + + + + + Simple Map + + + + + + +
+
+ +
+
+ +
+
+
+ + + + + + + diff --git a/samples/weather-api-current-compact/index.ts b/samples/weather-api-current-compact/index.ts new file mode 100644 index 00000000..87badd21 --- /dev/null +++ b/samples/weather-api-current-compact/index.ts @@ -0,0 +1,342 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_weather_api_compact] +import './simple-weather-widget'; // Import the custom element + +const CURRENT_CONDITIONS_API_URL = 'https://weather.googleapis.com/v1/currentConditions:lookup'; // Current Conditions API endpoint. +const API_KEY = "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8"; // Use the hardcoded API key from index.html +const LIGHT_MAP_ID = 'c306b3c6dd3ed8d9'; +const DARK_MAP_ID = '6b73a9fe7e831a00'; + +let map: google.maps.Map; +let activeWeatherWidget: SimpleWeatherWidget | null = null; // To keep track of the currently active widget +let allMarkers: google.maps.marker.AdvancedMarkerElement[] = []; // To store all active markers +let markersLoaded = false; // Flag to track if button markers are loaded + +async function initMap(): Promise { + const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary; + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; + + map = new Map(document.getElementById("map") as HTMLElement, { + center: { lat: 48.8566, lng: 2.3522 }, // Set center to Paris initially, will change based on markers + zoom: 6, + minZoom: 5, // Set minimum zoom level to 5 + disableDefaultUI: true, // Disable default UI on basemap click + mapId: 'c306b3c6dd3ed8d9', // Use the specified map ID for light mode + clickableIcons: false, // Disable clicks on base map POIs + }); + + // Load a marker at the initial map center + const initialCenter = map.getCenter(); + if (initialCenter) { + await createAndAddMarker({ name: 'Initial Location', lat: initialCenter.lat(), lng: initialCenter.lng() }, 'dynamic'); // Create and add dynamic marker at center + } + + + // Add a click listener to the map to handle creating a new marker or hiding the active widget + map.addListener('click', async (event: google.maps.MapMouseEvent) => { + // Check if the click was on a marker. If so, the marker's own click listener will handle it. + // If not, create a new dynamic marker or hide the active widget. + let target = event.domEvent.target as HTMLElement; + let isClickOnMarker = false; + while (target) { + if (target.tagName === 'SIMPLE-WEATHER-WIDGET' || target.classList.contains('gm-control-active')) { // Check for widget or default marker control + isClickOnMarker = true; + break; + } + target = target.parentElement as HTMLElement; + } + + if (!isClickOnMarker && event.latLng) { + // If a widget is active, hide its rain details and reset zIndex + if (activeWeatherWidget) { + const rainDetailsElement = activeWeatherWidget.shadowRoot!.getElementById('rain-details') as HTMLDivElement; + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot!.querySelector('.widget-container') as HTMLDivElement; + activeWidgetContainer.classList.remove('highlight'); + // Find the marker associated with the active widget and reset its zIndex + const activeMarker = allMarkers.find(marker => marker.content === activeWeatherWidget); + if (activeMarker) { + activeMarker.zIndex = null; + } + activeWeatherWidget = null; // Clear the active widget + } + + // Remove the previous dynamic marker if it exists + const currentDynamicMarkerIndex = allMarkers.findIndex(marker => (marker as any).markerType === 'dynamic'); + if (currentDynamicMarkerIndex !== -1) { + allMarkers[currentDynamicMarkerIndex].map = null; + allMarkers.splice(currentDynamicMarkerIndex, 1); + } + + // Create a new dynamic marker at the clicked location + await createAndAddMarker({ name: 'Clicked Location', lat: event.latLng.lat(), lng: event.latLng.lng() }, 'dynamic'); // Create and add dynamic marker + } + }); +} + +/** + * Creates a weather widget and marker and adds them to the map. + * @param location The location for the marker. + * @param markerType The type of marker ('initial', 'button', 'dynamic'). + */ +async function createAndAddMarker(location: { name: string; lat: number; lng: number; }, markerType: 'initial' | 'button' | 'dynamic'): Promise { + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; + + const weatherWidget = document.createElement('simple-weather-widget') as SimpleWeatherWidget; + + // Apply dark mode if the map container is in dark mode + const mapContainer = document.getElementById("map") as HTMLElement; + if (mapContainer.classList.contains('dark-mode')) { + weatherWidget.setMode('dark'); + } + + const marker = new AdvancedMarkerElement({ + map: map, + position: { lat: location.lat, lng: location.lng }, + content: weatherWidget, + title: location.name // Add a title for accessibility + }); + + // Store the marker type + (marker as any).markerType = markerType; + + // Fetch and update weather data for this location + updateWeatherDisplayForMarker(marker, weatherWidget, new google.maps.LatLng(location.lat, location.lng)); + + // Add click listener to the marker + marker.addListener('click', () => { + const widgetContainer = weatherWidget.shadowRoot!.querySelector('.widget-container') as HTMLDivElement; + + // If a widget is currently active and it's not the clicked one, remove its highlight class and reset zIndex + if (activeWeatherWidget && activeWeatherWidget !== weatherWidget) { + const activeWidgetContainer = activeWeatherWidget.shadowRoot!.querySelector('.widget-container') as HTMLDivElement; + activeWidgetContainer.classList.remove('highlight'); + // Find the marker associated with the active widget and reset its zIndex + const activeMarker = allMarkers.find(marker => marker.content === activeWeatherWidget); + if (activeMarker) { + activeMarker.zIndex = null; + } + } + + // Toggle the highlight class on the clicked widget's container + widgetContainer.classList.toggle('highlight'); + + // Update the activeWeatherWidget and set zIndex based on the highlight state + if (widgetContainer.classList.contains('highlight')) { + activeWeatherWidget = weatherWidget; + marker.zIndex = 1; // Set zIndex to 1 when highlighted + } else { + activeWeatherWidget = null; + marker.zIndex = null; // Reset zIndex when not highlighted + } + }); + + allMarkers.push(marker); // Add the marker to the allMarkers array +} + + +/** + * Toggles the visual mode of the weather widget and map between light and dark. + * Call this function to switch the mode. + */ +/** + * Toggles the dark mode class on the body element. + */ +async function toggleDarkMode() { + const mapContainer = document.getElementById("map") as HTMLElement; + mapContainer.classList.toggle('dark-mode'); + + const modeToggleButton = document.getElementById('mode-toggle'); + if (modeToggleButton) { + if (mapContainer.classList.contains('dark-mode')) { + modeToggleButton.textContent = 'Light Mode'; + } else { + modeToggleButton.textContent = 'Dark Mode'; + } + } + + // Remove all markers from the map + allMarkers.forEach(marker => { + marker.map = null; + }); + + // Re-initialize the map to apply the new map ID + const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary; + const currentCenter = map.getCenter(); + const currentZoom = map.getZoom(); + const currentMapId = mapContainer.classList.contains('dark-mode') ? DARK_MAP_ID : LIGHT_MAP_ID; + + map = new Map(mapContainer, { + center: currentCenter, + zoom: currentZoom, + minZoom: 5, // Set minimum zoom level to 5 + disableDefaultUI: true, + mapId: currentMapId, + clickableIcons: false, + }); + + // Re-add all markers to the new map instance and update their widget mode + const markersToReAdd = [...allMarkers]; // Create a copy to avoid modifying the array while iterating + allMarkers = []; // Clear the array before re-adding + + for (const marker of markersToReAdd) { + marker.map = map; // Add marker to the new map + const weatherWidget = marker.content as SimpleWeatherWidget; + const mapContainer = document.getElementById("map") as HTMLElement; // Re-get map container + if (mapContainer.classList.contains('dark-mode')) { + weatherWidget.setMode('dark'); + } else { + weatherWidget.setMode('light'); + } + allMarkers.push(marker); // Add back to the allMarkers array + } + + + // Re-add the map click listener + map.addListener('click', async (event: google.maps.MapMouseEvent) => { + // Check if the click was on a marker. If so, the marker's own click listener will handle it. + // If not, create a new dynamic marker or hide the active widget. + let target = event.domEvent.target as HTMLElement; + let isClickOnMarker = false; + while (target) { + if (target.tagName === 'SIMPLE-WEATHER-WIDGET' || target.classList.contains('gm-control-active')) { // Check for widget or default marker control + isClickOnMarker = true; + break; + } + target = target.parentElement as HTMLElement; + } + + if (!isClickOnMarker && event.latLng) { + if (activeWeatherWidget) { + const rainDetailsElement = activeWeatherWidget.shadowRoot!.getElementById('rain-details') as HTMLDivElement; + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot!.querySelector('.widget-container') as HTMLDivElement; + activeWidgetContainer.classList.remove('highlight'); + // Find the marker associated with the active widget and reset its zIndex + const activeMarker = allMarkers.find(marker => marker.content === activeWeatherWidget); + if (activeMarker) { + activeMarker.zIndex = null; + } + activeWeatherWidget = null; // Clear the active widget + } + + // Remove the previous dynamic marker if it exists + const currentDynamicMarkerIndex = allMarkers.findIndex(marker => (marker as any).markerType === 'dynamic'); + if (currentDynamicMarkerIndex !== -1) { + allMarkers[currentDynamicMarkerIndex].map = null; + allMarkers.splice(currentDynamicMarkerIndex, 1); + } + + // Create a new dynamic marker at the clicked location + await createAndAddMarker({ name: 'Clicked Location', lat: event.latLng.lat(), lng: event.latLng.lng() }, 'dynamic'); // Create and add dynamic marker + } + }); +} + +const locations = [ + { name: 'London', lat: 51.5074, lng: -0.1278 }, + { name: 'Brussels', lat: 50.8503, lng: 4.3517 }, + { name: 'Luxembourg', lat: 49.8153, lng: 6.1296 }, + { name: 'Amsterdam', lat: 52.3676, lng: 4.9041 }, + { name: 'Berlin', lat: 52.5200, lng: 13.4050 }, + { name: 'Rome', lat: 41.9028, lng: 12.4964 }, + { name: 'Geneva', lat: 46.2044, lng:6.14324 }, + { name: 'Barcelona', lat:41.3874, lng: -2.1686}, + { name: 'Milan', lat:45.4685, lng:9.1824}, +]; + +async function loadWeatherMarkers(): Promise { + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; + + for (const location of locations) { + await createAndAddMarker(location, 'button'); // Create and add button markers + } +} + +function removeButtonMarkers(): void { + // If a button marker widget is active, hide its rain details and reset zIndex + if (activeWeatherWidget) { + const buttonMarker = allMarkers.find(marker => marker.content === activeWeatherWidget && (marker as any).markerType === 'button'); + if (buttonMarker) { + const rainDetailsElement = activeWeatherWidget.shadowRoot!.getElementById('rain-details') as HTMLDivElement; + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot!.querySelector('.widget-container') as HTMLDivElement; + activeWidgetContainer.classList.remove('highlight'); + buttonMarker.zIndex = null; + activeWeatherWidget = null; // Clear the active widget + } + } + + // Remove button markers from the map and the allMarkers array + const markersToRemove = allMarkers.filter(marker => (marker as any).markerType === 'button'); + markersToRemove.forEach(marker => { + marker.map = null; + const index = allMarkers.indexOf(marker); + if (index > -1) { + allMarkers.splice(index, 1); + } + }); +} + + +async function updateWeatherDisplayForMarker(marker: google.maps.marker.AdvancedMarkerElement, widget: SimpleWeatherWidget, location: google.maps.LatLng): Promise { + const lat = location.lat(); + const lng = location.lng(); + + const currentConditionsUrl = `${CURRENT_CONDITIONS_API_URL}?key=${API_KEY}&location.latitude=${lat}&location.longitude=${lng}`; + + try { + const response = await fetch(currentConditionsUrl); + + if (!response.ok) { + const errorBody = await response.json(); + console.error('API error response:', errorBody); + + if (response.status === 404 && errorBody?.error?.status === 'NOT_FOUND') { + widget.data = { error: "Location not supported" }; + } else { + throw new Error(`HTTP error! status: ${response.status}`); + } + } else { + const weatherData = await response.json(); + console.log('Weather data fetched for marker:', weatherData); + widget.data = weatherData; + } + } catch (error) { + console.error('Error fetching weather data for marker:', error); + widget.data = { error: "Failed to fetch weather data" }; + } +} + +initMap(); + +// Wait for the custom element to be defined before adding the event listener +customElements.whenDefined('simple-weather-widget').then(() => { + const modeToggleButton = document.getElementById('mode-toggle'); + if (modeToggleButton) { + modeToggleButton.addEventListener('click', () => { + toggleDarkMode(); + }); + } + + const loadMarkersButton = document.getElementById('load-markers-button'); + if (loadMarkersButton) { + loadMarkersButton.addEventListener('click', () => { + if (!markersLoaded) { + loadWeatherMarkers(); + markersLoaded = true; + loadMarkersButton.textContent = 'Remove Markers'; + } else { + removeButtonMarkers(); + markersLoaded = false; + loadMarkersButton.textContent = 'Load Markers'; + } + }); + } +}); +// [END maps_weather_api_compact] \ No newline at end of file diff --git a/samples/weather-api-current-compact/package.json b/samples/weather-api-current-compact/package.json new file mode 100644 index 00000000..e145ecb6 --- /dev/null +++ b/samples/weather-api-current-compact/package.json @@ -0,0 +1,14 @@ +{ + "name": "@js-api-samples/weather-api-compact", + "version": "1.0.0", + "scripts": { + "build": "tsc && bash ../jsfiddle.sh map-simple && bash ../app.sh map-simple && bash ../docs.sh map-simple && npm run build:vite --workspace=. && bash ../dist.sh map-simple", + "test": "tsc && npm run build:vite --workspace=.", + "start": "vite --port 5173", + "build:vite": "vite build --base './'", + "preview": "vite preview" + }, + "dependencies": { + + } +} \ No newline at end of file diff --git a/samples/weather-api-current-compact/simple-weather-widget.ts b/samples/weather-api-current-compact/simple-weather-widget.ts new file mode 100644 index 00000000..5b5b8444 --- /dev/null +++ b/samples/weather-api-current-compact/simple-weather-widget.ts @@ -0,0 +1,252 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +class SimpleWeatherWidget extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot!.innerHTML = ` + + +
+
+ Weather Icon + +
+
+ Rain Probability Icon + + +
+
+ `; + } + + set data(weatherData: any) { + const iconElement = this.shadowRoot!.getElementById('condition-icon') as HTMLImageElement; + const temperatureElement = this.shadowRoot!.getElementById('temperature') as HTMLSpanElement; + const rainProbabilityElement = this.shadowRoot!.getElementById('rain-probability') as HTMLSpanElement; + const rainQpfElement = this.shadowRoot!.getElementById('rain-qpf') as HTMLSpanElement; + const rainDetailsElement = this.shadowRoot!.getElementById('rain-details') as HTMLDivElement; + + + if (!weatherData || weatherData.error) { + iconElement.style.display = 'none'; + rainDetailsElement.style.display = 'none'; + if (weatherData && weatherData.error) { + temperatureElement.textContent = weatherData.error; + temperatureElement.classList.add('error-message'); // Add error class + } else { + temperatureElement.textContent = 'N/A'; + temperatureElement.classList.remove('error-message'); // Remove error class + } + return; + } + + // Check if the data is current conditions or forecast day structure + const isForecastDay = weatherData.daytimeForecast || weatherData.nighttimeForecast; + + let temperature: number | undefined, iconBaseUri: string | undefined, rainProbability: number | undefined, rainQpf: number | undefined; + + if (isForecastDay) { + // Data is a forecast day object + const conditions = weatherData; + temperature = conditions.maxTemperature?.degrees; + iconBaseUri = conditions.daytimeForecast?.weatherCondition?.iconBaseUri || conditions.nighttimeForecast?.weatherCondition?.iconBaseUri; + rainProbability = conditions.precipitation?.probability?.percent; + rainQpf = conditions.precipitation?.qpf?.quantity; + } else { + // Data is a current conditions object + const conditions = weatherData; + temperature = conditions.temperature?.degrees; + iconBaseUri = conditions.weatherCondition?.iconBaseUri; + rainProbability = conditions.precipitation?.probability?.percent; + // For current conditions, prioritize qpf from history if available + rainQpf = conditions.currentConditionsHistory?.qpf?.quantity !== undefined ? conditions.currentConditionsHistory.qpf.quantity : conditions.precipitation?.qpf?.quantity; + } + + let iconSrc = ''; // Initialize iconSrc + + if (iconBaseUri) { + // Use the full iconBaseUri and append .svg + iconSrc = `${iconBaseUri}.svg`; + } else { + // Fallback to a default local icon if iconBaseUri is not available + iconSrc = '/icons/cloud-cover-white.svg'; + } + + iconElement.style.display = 'none'; // Explicitly hide the icon before setting src + iconElement.onload = () => { + iconElement.style.display = 'inline-block'; // Show the icon after loading + }; + iconElement.onerror = () => { + console.error('Failed to load weather icon:', iconSrc); + iconElement.style.display = 'none'; // Hide the icon if loading fails + }; + iconElement.src = iconSrc; + + + temperatureElement.textContent = `${temperature !== undefined ? temperature.toFixed(0) : 'N/A'}°C`; // Rounded temperature + temperatureElement.classList.remove('error-message'); // Remove error class if data is valid + + if (rainProbability !== undefined && rainProbability !== null) { + rainProbabilityElement.textContent = `${rainProbability}%`; + } else { + rainProbabilityElement.textContent = '0%'; + } + + if (rainQpf !== undefined && rainQpf !== null) { + rainQpfElement.textContent = `${rainQpf.toFixed(1)}mm`; // Rounded QPF to 1 decimal place + } else { + rainQpfElement.textContent = '0.0mm'; // Display 0.0mm if data is not available + } + } + + /** + * Sets the visual mode of the widget. + * @param mode 'light' or 'dark' + */ + setMode(mode: 'light' | 'dark') { + if (mode === 'dark') { + this.classList.add('dark-mode'); + } else { + this.classList.remove('dark-mode'); + } + } +} + +customElements.define('simple-weather-widget', SimpleWeatherWidget); \ No newline at end of file diff --git a/samples/weather-api-current-compact/style.css b/samples/weather-api-current-compact/style.css new file mode 100644 index 00000000..70674b39 --- /dev/null +++ b/samples/weather-api-current-compact/style.css @@ -0,0 +1,240 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_map_simple] */ +/* + * Always set the map height explicitly to define the size of the div element + * that contains the map. + */ +#map { + height: 100%; +} + +/* + * Optional: Makes the sample page fill the window. + */ +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* [END maps_map_simple] */ + +/* Styles for the weather widget */ +.widget-container { + background-color: white; /* Light mode background */ + color: #222222; /* Light mode text color */ + padding: 4px 8px; /* Adjust padding */ + border-radius: 50px; /* Adjusted border-radius for round shape */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); /* Light mode box shadow */ + font-family: 'Google Sans', Roboto, sans-serif; /* Using the requested font stack */ + width: auto; /* Allow width to adjust to content */ + text-align: center; + position: relative; /* Needed to position arrow relative to this container */ + min-width: 78px; /* Adjusted minimum width as requested */ + min-height: 30px; /* Adjusted minimum height as requested by user */ + display: flex; /* Use flexbox for centering */ + flex-direction: column; /* Stack children vertically */ + justify-content: flex-start; /* Align content to the top initially */ + align-items: center; /* Horizontally center content */ + user-select: none; /* Prevent text selection */ + transition: max-height 0.3s ease-out, padding 0.3s ease-out, border-radius 0.3s ease-out; /* Add transition for max-height and padding */ + overflow: hidden; /* Hide overflowing content during transition */ + box-sizing: border-box; /* Include padding and border in the element's total width and height */ + max-height: 50px; /* Set max-height for default state */ +} + +/* Arrow indent */ +.widget-container::after { + content: ''; + position: absolute; + bottom: -5px; /* Position below the widget container */ + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid white; /* Match background color of widget-container */ + transition: all 0.3s ease-out; /* Add transition for smooth arrow movement */ +} + +/* Dark mode styles */ +.dark-mode .widget-container { + background-color: #222222; /* Dark mode background */ + color: white; /* Dark mode text color */ + box-shadow: 0 2px 6px rgba(255, 255, 255, 0.3); /* Dark mode box shadow */ +} + +.dark-mode .widget-container::after { + border-top-color: #222222; /* Match dark mode background color */ +} + +.weather-info-basic { + display: flex; + align-items: center; + justify-content: center; /* Center items */ + gap: 4px; /* Add gap between temperature and icon */ + margin-bottom: 0; /* Remove bottom margin */ + width: 100%; /* Take full width */ +} +.weather-info-basic img { + width: 30px; + height: 30px; + filter: invert(0); /* Default filter for light mode */ + flex-shrink: 0; /* Prevent shrinking */ +} +#condition-icon { + display: none; /* Hide the image by default */ +} +.temperature { + font-size: 1.5em; /* Adjust font size */ + font-weight: bold; +} +.error-message { + font-size: 1.2em; /* Font size for error messages as requested */ + font-weight: normal; /* Not bold for error messages */ + width:80px; +} +.rain-details { + font-size: 0.9em; /* Match detail line font size */ + display: none; /* Hide by default */ + align-items: center; + justify-content: flex-start; /* Align rain info to the left */ + flex-direction: row; /* Arrange rain details horizontally */ + gap: 5px; /* Space between rain probability and qpf */ + margin-top: 5px; /* Add space above rain details */ + width: 100%; /* Take full width */ +} + .rain-details img { + width: 18px; /* Adjust icon size */ + height: 18px; + margin-right: 5px; + } + +/* Dark mode rain icon filter */ +.dark-mode .rain-details img { + filter: none; /* Remove filter in dark mode */ +} + + +/* Highlighted state styles (on click) */ +.widget-container.highlight { + border-radius: 8px; /* Match non-highlighted border-radius */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); /* Keep the same box shadow */ + max-height: 150px; /* Set a larger max-height for expanded state */ + padding: 10px 15px; /* Keep the same padding */ + width: auto; /* Allow width to expand */ + min-height: 70px; /* Increase min-height for expanded state */ + justify-content: space-between; /* Space out basic and rain info */ +} + +.widget-container.highlight::after { + border-top: 5px solid white; /* Match background color */ +} + +.widget-container.highlight .rain-details { + display: flex; /* Show rain details when highlighted */ +} + +/* Dark mode highlighted state */ +.dark-mode .widget-container.highlight { + box-shadow: 0 2px 6px rgba(255, 255, 255, 0.3); /* Keep the same box shadow */ +} + +.dark-mode .widget-container.highlight::after { + border-top: 5px solid #222222; /* Match dark mode background color */ +} + +/* Styles for the button container wrapper */ +.button-container-wrapper { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + z-index: 10; /* Ensure it's above the map */ + display: flex; + gap: 10px; /* Space between buttons */ +} + +/* Remove absolute positioning from individual button containers */ +.mode-toggle-container, +.load-markers-container { + position: static; + top: auto; + left: auto; + transform: none; + z-index: auto; +} + +/* Common styles for the buttons */ +.button-container-wrapper button { + background-color: #4285F4; /* Google Blue */ + color: white; /* White text for contrast */ + border: none; + padding: 8px 15px; + border-radius: 4px; + cursor: pointer; + font-family: 'Google Sans', Roboto, sans-serif; + font-size: 1em; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + width:170px; +} + +/* Hover style for the buttons */ +.button-container-wrapper button:hover { + background-color: #3367D6; /* Darker shade on hover */ +} + +/* Media query for mobile devices */ +@media (max-width: 600px) { + .widget-container { + padding: 3px 5px; /* Reduce padding */ + min-width: 60px; /* Reduce min-width */ + min-height: 25px; /* Reduce min-height */ + max-height: 40px; /* Adjust max-height */ + } + + .weather-info-basic img { + width: 25px; /* Reduce icon size */ + height: 25px; + } + + .temperature { + font-size: 1.2em; /* Reduce font size */ + } + + .rain-details { + font-size: 0.8em; /* Reduce font size */ + gap: 3px; /* Reduce gap */ + margin-top: 3px; /* Reduce margin-top */ + } + + .rain-details img { + width: 15px; /* Reduce icon size */ + height: 15px; + margin-right: 3px; /* Reduce margin-right */ + } + + .widget-container.highlight { + max-height: 100px; /* Adjust max-height for expanded state */ + padding: 8px 10px; /* Adjust padding */ + min-height: 50px; /* Adjust min-height */ + } + + .button-container-wrapper { + flex-direction: column; /* Stack buttons vertically */ + gap: 5px; /* Reduce gap between buttons */ + bottom: 5px; /* Adjust bottom position */ + } + + .button-container-wrapper button { + width: 150px; /* Adjust button width */ + padding: 6px 10px; /* Adjust button padding */ + font-size: 0.9em; /* Adjust button font size */ + } +} \ No newline at end of file diff --git a/samples/weather-api-current-compact/tsconfig.json b/samples/weather-api-current-compact/tsconfig.json new file mode 100644 index 00000000..f4060794 --- /dev/null +++ b/samples/weather-api-current-compact/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "strict": true, + "noImplicitAny": false, + "lib": [ + "es2015", + "esnext", + "es6", + "dom", + "dom.iterable" + ], + "moduleResolution": "Node", + "jsx": "preserve", + "types": ["@types/google.maps"] + } +} \ No newline at end of file