diff --git a/dist/samples/weather-api-current-compact/app/.eslintsrc.json b/dist/samples/weather-api-current-compact/app/.eslintsrc.json new file mode 100644 index 00000000..4c44dab0 --- /dev/null +++ b/dist/samples/weather-api-current-compact/app/.eslintsrc.json @@ -0,0 +1,13 @@ +{ + "extends": [ + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "rules": { + "@typescript-eslint/ban-ts-comment": 0, + "@typescript-eslint/no-this-alias": 1, + "@typescript-eslint/no-empty-function": 1, + "@typescript-eslint/explicit-module-boundary-types": 1, + "@typescript-eslint/no-unused-vars": 1 + } +} diff --git a/dist/samples/weather-api-current-compact/app/README.md b/dist/samples/weather-api-current-compact/app/README.md new file mode 100644 index 00000000..be15c0d7 --- /dev/null +++ b/dist/samples/weather-api-current-compact/app/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/dist/samples/weather-api-current-compact/app/index.html b/dist/samples/weather-api-current-compact/app/index.html new file mode 100644 index 00000000..1e3c66a4 --- /dev/null +++ b/dist/samples/weather-api-current-compact/app/index.html @@ -0,0 +1,33 @@ + + + + + + Simple Map + + + + + + +
+
+ +
+
+ +
+
+
+ + + + + + + diff --git a/dist/samples/weather-api-current-compact/app/index.ts b/dist/samples/weather-api-current-compact/app/index.ts new file mode 100644 index 00000000..87badd21 --- /dev/null +++ b/dist/samples/weather-api-current-compact/app/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/dist/samples/weather-api-current-compact/app/package.json b/dist/samples/weather-api-current-compact/app/package.json new file mode 100644 index 00000000..1b782ada --- /dev/null +++ b/dist/samples/weather-api-current-compact/app/package.json @@ -0,0 +1,14 @@ +{ + "name": "@js-api-samples/weather-api-current-compact", + "version": "1.0.0", + "scripts": { + "build": "tsc && bash ../jsfiddle.sh weather-api-current-compact && bash ../app.sh weather-api-current-compact && bash ../docs.sh weather-api-current-compact && npm run build:vite --workspace=. && bash ../dist.sh weather-api-current-compact", + "test": "tsc && npm run build:vite --workspace=.", + "start": "vite --port 5173", + "build:vite": "vite build --base './'", + "preview": "vite preview" + }, + "dependencies": { + + } +} diff --git a/dist/samples/weather-api-current-compact/app/style.css b/dist/samples/weather-api-current-compact/app/style.css new file mode 100644 index 00000000..46a561c4 --- /dev/null +++ b/dist/samples/weather-api-current-compact/app/style.css @@ -0,0 +1,239 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_weather_api_current_compact] */ +/* + * 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; +} + +/* 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 */ + } +} +/* [END maps_weather_api_current_compact] */ diff --git a/dist/samples/weather-api-current-compact/app/tsconfig.json b/dist/samples/weather-api-current-compact/app/tsconfig.json new file mode 100644 index 00000000..f4060794 --- /dev/null +++ b/dist/samples/weather-api-current-compact/app/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 diff --git a/dist/samples/weather-api-current-compact/dist/assets/index-C4J1FsN0.css b/dist/samples/weather-api-current-compact/dist/assets/index-C4J1FsN0.css new file mode 100644 index 00000000..f5eb75fb --- /dev/null +++ b/dist/samples/weather-api-current-compact/dist/assets/index-C4J1FsN0.css @@ -0,0 +1,5 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */#map{height:100%}html,body{height:100%;margin:0;padding:0}.widget-container{background-color:#fff;color:#222;padding:4px 8px;border-radius:50px;box-shadow:0 2px 6px #0000004d;font-family:Google Sans,Roboto,sans-serif;width:auto;text-align:center;position:relative;min-width:78px;min-height:30px;display:flex;flex-direction:column;justify-content:flex-start;align-items:center;-webkit-user-select:none;user-select:none;transition:max-height .3s ease-out,padding .3s ease-out,border-radius .3s ease-out;overflow:hidden;box-sizing:border-box;max-height:50px}.widget-container:after{content:"";position:absolute;bottom:-5px;left:50%;transform:translate(-50%);width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid white;transition:all .3s ease-out}.dark-mode .widget-container{background-color:#222;color:#fff;box-shadow:0 2px 6px #ffffff4d}.dark-mode .widget-container:after{border-top-color:#222}.weather-info-basic{display:flex;align-items:center;justify-content:center;gap:4px;margin-bottom:0;width:100%}.weather-info-basic img{width:30px;height:30px;filter:invert(0);flex-shrink:0}#condition-icon{display:none}.temperature{font-size:1.5em;font-weight:700}.error-message{font-size:1.2em;font-weight:400;width:80px}.rain-details{font-size:.9em;display:none;align-items:center;justify-content:flex-start;flex-direction:row;gap:5px;margin-top:5px;width:100%}.rain-details img{width:18px;height:18px;margin-right:5px}.dark-mode .rain-details img{filter:none}.widget-container.highlight{border-radius:8px;box-shadow:0 2px 6px #0000004d;max-height:150px;padding:10px 15px;width:auto;min-height:70px;justify-content:space-between}.widget-container.highlight:after{border-top:5px solid white}.widget-container.highlight .rain-details{display:flex}.dark-mode .widget-container.highlight{box-shadow:0 2px 6px #ffffff4d}.dark-mode .widget-container.highlight:after{border-top:5px solid #222222}.button-container-wrapper{position:absolute;bottom:10px;left:50%;transform:translate(-50%);z-index:10;display:flex;gap:10px}.mode-toggle-container,.load-markers-container{position:static;top:auto;left:auto;transform:none;z-index:auto}.button-container-wrapper button{background-color:#4285f4;color:#fff;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 #0003;width:170px}.button-container-wrapper button:hover{background-color:#3367d6}@media (max-width: 600px){.widget-container{padding:3px 5px;min-width:60px;min-height:25px;max-height:40px}.weather-info-basic img{width:25px;height:25px}.temperature{font-size:1.2em}.rain-details{font-size:.8em;gap:3px;margin-top:3px}.rain-details img{width:15px;height:15px;margin-right:3px}.widget-container.highlight{max-height:100px;padding:8px 10px;min-height:50px}.button-container-wrapper{flex-direction:column;gap:5px;bottom:5px}.button-container-wrapper button{width:150px;padding:6px 10px;font-size:.9em}} diff --git a/dist/samples/weather-api-current-compact/dist/assets/index-DRmuvMHI.js b/dist/samples/weather-api-current-compact/dist/assets/index-DRmuvMHI.js new file mode 100644 index 00000000..e0091932 --- /dev/null +++ b/dist/samples/weather-api-current-compact/dist/assets/index-DRmuvMHI.js @@ -0,0 +1,154 @@ +(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))i(t);new MutationObserver(t=>{for(const o of t)if(o.type==="childList")for(const c of o.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&i(c)}).observe(document,{childList:!0,subtree:!0});function n(t){const o={};return t.integrity&&(o.integrity=t.integrity),t.referrerPolicy&&(o.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?o.credentials="include":t.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function i(t){if(t.ep)return;t.ep=!0;const o=n(t);fetch(t.href,o)}})();/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */class P extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.shadowRoot.innerHTML=` + + +
+
+ Weather Icon + +
+
+ Rain Probability Icon + + +
+
+ `}set data(e){var y,w,k,L,v,E,I,C,M,A,R,B,T,W,S,z,D;const n=this.shadowRoot.getElementById("condition-icon"),i=this.shadowRoot.getElementById("temperature"),t=this.shadowRoot.getElementById("rain-probability"),o=this.shadowRoot.getElementById("rain-qpf"),c=this.shadowRoot.getElementById("rain-details");if(!e||e.error){n.style.display="none",c.style.display="none",e&&e.error?(i.textContent=e.error,i.classList.add("error-message")):(i.textContent="N/A",i.classList.remove("error-message"));return}const a=e.daytimeForecast||e.nighttimeForecast;let r,m,h,p;if(a){const g=e;r=(y=g.maxTemperature)==null?void 0:y.degrees,m=((k=(w=g.daytimeForecast)==null?void 0:w.weatherCondition)==null?void 0:k.iconBaseUri)||((v=(L=g.nighttimeForecast)==null?void 0:L.weatherCondition)==null?void 0:v.iconBaseUri),h=(I=(E=g.precipitation)==null?void 0:E.probability)==null?void 0:I.percent,p=(M=(C=g.precipitation)==null?void 0:C.qpf)==null?void 0:M.quantity}else{const g=e;r=(A=g.temperature)==null?void 0:A.degrees,m=(R=g.weatherCondition)==null?void 0:R.iconBaseUri,h=(T=(B=g.precipitation)==null?void 0:B.probability)==null?void 0:T.percent,p=((S=(W=g.currentConditionsHistory)==null?void 0:W.qpf)==null?void 0:S.quantity)!==void 0?g.currentConditionsHistory.qpf.quantity:(D=(z=g.precipitation)==null?void 0:z.qpf)==null?void 0:D.quantity}let u="";m?u=`${m}.svg`:u="/icons/cloud-cover-white.svg",n.style.display="none",n.onload=()=>{n.style.display="inline-block"},n.onerror=()=>{console.error("Failed to load weather icon:",u),n.style.display="none"},n.src=u,i.textContent=`${r!==void 0?r.toFixed(0):"N/A"}°C`,i.classList.remove("error-message"),h!=null?t.textContent=`${h}%`:t.textContent="0%",p!=null?o.textContent=`${p.toFixed(1)}mm`:o.textContent="0.0mm"}setMode(e){e==="dark"?this.classList.add("dark-mode"):this.classList.remove("dark-mode")}}customElements.define("simple-weather-widget",P);/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */const q="https://weather.googleapis.com/v1/currentConditions:lookup",N="AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8",O="c306b3c6dd3ed8d9",F="6b73a9fe7e831a00";let f,s=null,d=[],x=!1;async function H(){const{Map:l}=await google.maps.importLibrary("maps"),{AdvancedMarkerElement:e}=await google.maps.importLibrary("marker");f=new l(document.getElementById("map"),{center:{lat:48.8566,lng:2.3522},zoom:6,minZoom:5,disableDefaultUI:!0,mapId:"c306b3c6dd3ed8d9",clickableIcons:!1});const n=f.getCenter();n&&await b({name:"Initial Location",lat:n.lat(),lng:n.lng()},"dynamic"),f.addListener("click",async i=>{let t=i.domEvent.target,o=!1;for(;t;){if(t.tagName==="SIMPLE-WEATHER-WIDGET"||t.classList.contains("gm-control-active")){o=!0;break}t=t.parentElement}if(!o&&i.latLng){if(s){const a=s.shadowRoot.getElementById("rain-details");a.style.display="none",s.shadowRoot.querySelector(".widget-container").classList.remove("highlight");const m=d.find(h=>h.content===s);m&&(m.zIndex=null),s=null}const c=d.findIndex(a=>a.markerType==="dynamic");c!==-1&&(d[c].map=null,d.splice(c,1)),await b({name:"Clicked Location",lat:i.latLng.lat(),lng:i.latLng.lng()},"dynamic")}})}async function b(l,e){const{AdvancedMarkerElement:n}=await google.maps.importLibrary("marker"),i=document.createElement("simple-weather-widget");document.getElementById("map").classList.contains("dark-mode")&&i.setMode("dark");const o=new n({map:f,position:{lat:l.lat,lng:l.lng},content:i,title:l.name});o.markerType=e,K(o,i,new google.maps.LatLng(l.lat,l.lng)),o.addListener("click",()=>{const c=i.shadowRoot.querySelector(".widget-container");if(s&&s!==i){s.shadowRoot.querySelector(".widget-container").classList.remove("highlight");const r=d.find(m=>m.content===s);r&&(r.zIndex=null)}c.classList.toggle("highlight"),c.classList.contains("highlight")?(s=i,o.zIndex=1):(s=null,o.zIndex=null)}),d.push(o)}async function U(){const l=document.getElementById("map");l.classList.toggle("dark-mode");const e=document.getElementById("mode-toggle");e&&(l.classList.contains("dark-mode")?e.textContent="Light Mode":e.textContent="Dark Mode"),d.forEach(a=>{a.map=null});const{Map:n}=await google.maps.importLibrary("maps"),i=f.getCenter(),t=f.getZoom(),o=l.classList.contains("dark-mode")?F:O;f=new n(l,{center:i,zoom:t,minZoom:5,disableDefaultUI:!0,mapId:o,clickableIcons:!1});const c=[...d];d=[];for(const a of c){a.map=f;const r=a.content;document.getElementById("map").classList.contains("dark-mode")?r.setMode("dark"):r.setMode("light"),d.push(a)}f.addListener("click",async a=>{let r=a.domEvent.target,m=!1;for(;r;){if(r.tagName==="SIMPLE-WEATHER-WIDGET"||r.classList.contains("gm-control-active")){m=!0;break}r=r.parentElement}if(!m&&a.latLng){if(s){const p=s.shadowRoot.getElementById("rain-details");p.style.display="none",s.shadowRoot.querySelector(".widget-container").classList.remove("highlight");const y=d.find(w=>w.content===s);y&&(y.zIndex=null),s=null}const h=d.findIndex(p=>p.markerType==="dynamic");h!==-1&&(d[h].map=null,d.splice(h,1)),await b({name:"Clicked Location",lat:a.latLng.lat(),lng:a.latLng.lng()},"dynamic")}})}const _=[{name:"London",lat:51.5074,lng:-.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.52,lng:13.405},{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 $(){const{AdvancedMarkerElement:l}=await google.maps.importLibrary("marker");for(const e of _)await b(e,"button")}function j(){if(s){const e=d.find(n=>n.content===s&&n.markerType==="button");if(e){const n=s.shadowRoot.getElementById("rain-details");n.style.display="none",s.shadowRoot.querySelector(".widget-container").classList.remove("highlight"),e.zIndex=null,s=null}}d.filter(e=>e.markerType==="button").forEach(e=>{e.map=null;const n=d.indexOf(e);n>-1&&d.splice(n,1)})}async function K(l,e,n){var c;const i=n.lat(),t=n.lng(),o=`${q}?key=${N}&location.latitude=${i}&location.longitude=${t}`;try{const a=await fetch(o);if(a.ok){const r=await a.json();console.log("Weather data fetched for marker:",r),e.data=r}else{const r=await a.json();if(console.error("API error response:",r),a.status===404&&((c=r==null?void 0:r.error)==null?void 0:c.status)==="NOT_FOUND")e.data={error:"Location not supported"};else throw new Error(`HTTP error! status: ${a.status}`)}}catch(a){console.error("Error fetching weather data for marker:",a),e.data={error:"Failed to fetch weather data"}}}H();customElements.whenDefined("simple-weather-widget").then(()=>{const l=document.getElementById("mode-toggle");l&&l.addEventListener("click",()=>{U()});const e=document.getElementById("load-markers-button");e&&e.addEventListener("click",()=>{x?(j(),x=!1,e.textContent="Load Markers"):($(),x=!0,e.textContent="Remove Markers")})}); diff --git a/dist/samples/weather-api-current-compact/dist/index.html b/dist/samples/weather-api-current-compact/dist/index.html new file mode 100644 index 00000000..08d038f3 --- /dev/null +++ b/dist/samples/weather-api-current-compact/dist/index.html @@ -0,0 +1,33 @@ + + + + + + Simple Map + + + + + + +
+
+ +
+
+ +
+
+
+ + + + + + + diff --git a/dist/samples/weather-api-current-compact/docs/index.html b/dist/samples/weather-api-current-compact/docs/index.html new file mode 100644 index 00000000..1e3c66a4 --- /dev/null +++ b/dist/samples/weather-api-current-compact/docs/index.html @@ -0,0 +1,33 @@ + + + + + + Simple Map + + + + + + +
+
+ +
+
+ +
+
+
+ + + + + + + diff --git a/dist/samples/weather-api-current-compact/docs/index.js b/dist/samples/weather-api-current-compact/docs/index.js new file mode 100644 index 00000000..856bcfe6 --- /dev/null +++ b/dist/samples/weather-api-current-compact/docs/index.js @@ -0,0 +1,300 @@ +/* + * @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; +let activeWeatherWidget = null; // To keep track of the currently active widget +let allMarkers = []; // To store all active markers +let markersLoaded = false; // Flag to track if button markers are loaded +async function initMap() { + const { Map } = await google.maps.importLibrary("maps"); + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker"); + map = new Map(document.getElementById("map"), { + 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) => { + // 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; + 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; + } + 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'); + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot.querySelector('.widget-container'); + 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.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, markerType) { + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker"); + const weatherWidget = document.createElement('simple-weather-widget'); + // Apply dark mode if the map container is in dark mode + const mapContainer = document.getElementById("map"); + 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.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'); + // 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'); + 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"); + 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"); + 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; + const mapContainer = document.getElementById("map"); // 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) => { + // 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; + 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; + } + if (!isClickOnMarker && event.latLng) { + if (activeWeatherWidget) { + const rainDetailsElement = activeWeatherWidget.shadowRoot.getElementById('rain-details'); + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot.querySelector('.widget-container'); + 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.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() { + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker"); + for (const location of locations) { + await createAndAddMarker(location, 'button'); // Create and add button markers + } +} +function removeButtonMarkers() { + // 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.markerType === 'button'); + if (buttonMarker) { + const rainDetailsElement = activeWeatherWidget.shadowRoot.getElementById('rain-details'); + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot.querySelector('.widget-container'); + 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.markerType === 'button'); + markersToRemove.forEach(marker => { + marker.map = null; + const index = allMarkers.indexOf(marker); + if (index > -1) { + allMarkers.splice(index, 1); + } + }); +} +async function updateWeatherDisplayForMarker(marker, widget, location) { + 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] diff --git a/dist/samples/weather-api-current-compact/docs/index.ts b/dist/samples/weather-api-current-compact/docs/index.ts new file mode 100644 index 00000000..87badd21 --- /dev/null +++ b/dist/samples/weather-api-current-compact/docs/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/dist/samples/weather-api-current-compact/docs/style.css b/dist/samples/weather-api-current-compact/docs/style.css new file mode 100644 index 00000000..46a561c4 --- /dev/null +++ b/dist/samples/weather-api-current-compact/docs/style.css @@ -0,0 +1,239 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_weather_api_current_compact] */ +/* + * 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; +} + +/* 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 */ + } +} +/* [END maps_weather_api_current_compact] */ diff --git a/dist/samples/weather-api-current-compact/jsfiddle/demo.css b/dist/samples/weather-api-current-compact/jsfiddle/demo.css new file mode 100644 index 00000000..27f5be33 --- /dev/null +++ b/dist/samples/weather-api-current-compact/jsfiddle/demo.css @@ -0,0 +1,239 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * 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; +} + +/* 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 */ + } +} + diff --git a/dist/samples/weather-api-current-compact/jsfiddle/demo.details b/dist/samples/weather-api-current-compact/jsfiddle/demo.details new file mode 100644 index 00000000..684c66d5 --- /dev/null +++ b/dist/samples/weather-api-current-compact/jsfiddle/demo.details @@ -0,0 +1,7 @@ +name: weather-api-current-compact +authors: + - Geo Developer IX Documentation Team +tags: + - google maps +load_type: h +description: Sample code supporting Google Maps Platform JavaScript API documentation. diff --git a/dist/samples/weather-api-current-compact/jsfiddle/demo.html b/dist/samples/weather-api-current-compact/jsfiddle/demo.html new file mode 100644 index 00000000..66ee371d --- /dev/null +++ b/dist/samples/weather-api-current-compact/jsfiddle/demo.html @@ -0,0 +1,33 @@ + + + + + + Simple Map + + + + + + +
+
+ +
+
+ +
+
+
+ + + + + + + diff --git a/dist/samples/weather-api-current-compact/jsfiddle/demo.js b/dist/samples/weather-api-current-compact/jsfiddle/demo.js new file mode 100644 index 00000000..1a855de0 --- /dev/null +++ b/dist/samples/weather-api-current-compact/jsfiddle/demo.js @@ -0,0 +1,300 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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; +let activeWeatherWidget = null; // To keep track of the currently active widget +let allMarkers = []; // To store all active markers +let markersLoaded = false; // Flag to track if button markers are loaded +async function initMap() { + const { Map } = await google.maps.importLibrary("maps"); + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker"); + map = new Map(document.getElementById("map"), { + 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) => { + // 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; + 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; + } + 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'); + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot.querySelector('.widget-container'); + 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.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, markerType) { + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker"); + const weatherWidget = document.createElement('simple-weather-widget'); + // Apply dark mode if the map container is in dark mode + const mapContainer = document.getElementById("map"); + 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.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'); + // 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'); + 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"); + 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"); + 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; + const mapContainer = document.getElementById("map"); // 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) => { + // 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; + 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; + } + if (!isClickOnMarker && event.latLng) { + if (activeWeatherWidget) { + const rainDetailsElement = activeWeatherWidget.shadowRoot.getElementById('rain-details'); + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot.querySelector('.widget-container'); + 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.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() { + const { AdvancedMarkerElement } = await google.maps.importLibrary("marker"); + for (const location of locations) { + await createAndAddMarker(location, 'button'); // Create and add button markers + } +} +function removeButtonMarkers() { + // 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.markerType === 'button'); + if (buttonMarker) { + const rainDetailsElement = activeWeatherWidget.shadowRoot.getElementById('rain-details'); + rainDetailsElement.style.display = 'none'; + const activeWidgetContainer = activeWeatherWidget.shadowRoot.querySelector('.widget-container'); + 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.markerType === 'button'); + markersToRemove.forEach(marker => { + marker.map = null; + const index = allMarkers.indexOf(marker); + if (index > -1) { + allMarkers.splice(index, 1); + } + }); +} +async function updateWeatherDisplayForMarker(marker, widget, location) { + 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'; + } + }); + } +}); + diff --git a/package-lock.json b/package-lock.json index ccaaf0c0..612bbf23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -633,7 +633,7 @@ "resolved": "samples/ui-kit-place-search-text", "link": true }, - "node_modules/@js-api-samples/weather-api-compact": { + "node_modules/@js-api-samples/weather-api-current-compact": { "resolved": "samples/weather-api-current-compact", "link": true }, @@ -2497,7 +2497,7 @@ "extraneous": true }, "samples/weather-api-current-compact": { - "name": "@js-api-samples/weather-api-compact", + "name": "@js-api-samples/weather-api-current-compact", "version": "1.0.0" } }