diff --git a/vehicle-map/app.js b/vehicle-map/app.js index 9223d52c..378cedc4 100644 --- a/vehicle-map/app.js +++ b/vehicle-map/app.js @@ -111,7 +111,7 @@ let exploreSuggestionItems = []; let activeExploreSuggestionIndex = -1; let detailExpanded = false; let navigationLaunchTimer = null; -const filterState = { province: '', city: '', district: '', query: '' }; +const filterState = { province: '', city: '', district: '', query: '', stationLocations: [] }; const REGION_ZOOM = { city: 7, district: 9.5, vehicle: 12 }; const MUNICIPALITIES = new Set(['北京', '天津', '上海', '重庆']); @@ -178,6 +178,24 @@ function stationMatchesQuery(station, query = filterState.query) { return [station?.name, station?.shortName, station?.address].some(value => normalizeSearch(value).includes(needle)); } +function stationLocationFilterKey(location) { + return [location?.province, location?.city, location?.district].map(value => String(value || '').trim()).join('|'); +} + +function stationMatchesLocationFilter(station, location) { + if (location?.province && station.province !== location.province) return false; + if (location?.city && station.city !== location.city) return false; + if (location?.district && stationDistrictName(station) !== location.district) return false; + return true; +} + +function stationLocationFilterLabel(location) { + const level = location?.district ? 'district' : location?.city ? 'city' : 'province'; + const raw = location?.[level] || ''; + if (currentLang === 'en') return administrativeEnglishName(raw, level); + return level === 'province' ? compactProvinceName(raw) : compactRegionName(raw); +} + function selectedVehicleFilterGroup() { if (filterState.district) return vehicleFilterGroups.districts.find(group => group.adcode === filterState.district) || null; if (filterState.city) return vehicleFilterGroups.cities.find(group => group.adcode === filterState.city) || null; @@ -196,16 +214,18 @@ function filteredVehicles(vehicles = dashboard?.vehicles || []) { } function filteredStations(stations = dashboard?.stations || []) { + const selectedLocations = filterState.stationLocations || []; return stations.filter(station => { - if (filterState.province && station.province !== filterState.province) return false; - if (filterState.city && station.city !== filterState.city) return false; - if (filterState.district && stationDistrictName(station) !== filterState.district) return false; + if (selectedLocations.length && !selectedLocations.some(location => stationMatchesLocationFilter(station, location))) return false; + if (!selectedLocations.length && filterState.province && station.province !== filterState.province) return false; + if (!selectedLocations.length && filterState.city && station.city !== filterState.city) return false; + if (!selectedLocations.length && filterState.district && stationDistrictName(station) !== filterState.district) return false; return stationMatchesQuery(station); }); } function isFilterActive() { - return Boolean(filterState.province || filterState.city || filterState.district || normalizeSearch(filterState.query)); + return Boolean(filterState.province || filterState.city || filterState.district || filterState.stationLocations?.length || normalizeSearch(filterState.query)); } function uniqueSortedOptions(values) { @@ -474,9 +494,18 @@ function closeExploreSuggestions() { async function selectExploreLocation(option) { filterState.query = ''; if (currentMode === 'station') { - filterState.province = option.province || ''; - filterState.city = option.city || ''; - filterState.district = option.district || ''; + const location = { + province: option.province || '', + city: option.city || '', + district: option.district || '' + }; + const key = stationLocationFilterKey(location); + if (!filterState.stationLocations.some(item => stationLocationFilterKey(item) === key)) filterState.stationLocations.push(location); + // Search selections are multi-select. The conventional dropdowns remain + // available as a separate, single-location filter path. + filterState.province = ''; + filterState.city = ''; + filterState.district = ''; } else { vehicleFilterGroups = { provinces: vehicleExploreCatalog.provinces.length ? vehicleExploreCatalog.provinces : vehicleFilterGroups.provinces, @@ -489,7 +518,7 @@ async function selectExploreLocation(option) { } closeExploreSuggestions(); updateFilterToolbar(); - focusCurrentRegion(option.level); + focusCurrentRegion(currentMode === 'station' && filterState.stationLocations.length > 1 ? 'province' : option.level); applyActiveFilters(); void syncFilterControls(dashboard); } @@ -542,11 +571,25 @@ function selectedLocationLabel(level) { function renderFilterChips() { const rail = document.getElementById('filterChipRail'); if (!rail) return; + if (currentMode === 'station' && filterState.stationLocations?.length) { + rail.hidden = false; + rail.innerHTML = filterState.stationLocations.map((location, index) => { + const label = stationLocationFilterLabel(location); + return `${escapeHTML(label)}`; + }).join(''); + return; + } const levels = ['province', 'city', 'district'].filter(level => filterState[level]); rail.hidden = !levels.length; rail.innerHTML = levels.map(level => `${escapeHTML(selectedLocationLabel(level))}`).join(''); } +function removeStationLocationFilter(index) { + if (currentMode !== 'station') return; + filterState.stationLocations.splice(index, 1); + applyActiveFilters(); +} + function clearLocationChip(level) { filterState[level] = ''; if (level === 'province') { filterState.city = ''; filterState.district = ''; } @@ -661,6 +704,7 @@ function clearNameFilter() { } async function handleRegionFilterChange(level, value) { + if (currentMode === 'station') filterState.stationLocations = []; filterState[level] = value; if (level === 'province') { filterState.city = ''; filterState.district = ''; } if (level === 'city') filterState.district = ''; @@ -670,7 +714,7 @@ async function handleRegionFilterChange(level, value) { } function clearAllFilters() { - filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; + filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; filterState.stationLocations = []; closeExploreSuggestions(); closeEntityDetails(); resetMapView(); @@ -1500,7 +1544,7 @@ function switchRankTab(type) { function switchMode(mode) { currentMode = mode; - filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; + filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; filterState.stationLocations = []; closeExploreSuggestions(); selectedEntity = null; const detailCard = document.getElementById('mapDetailCard'); @@ -1589,7 +1633,7 @@ function renderUserLocationMarker() { function handleLocationSuccess(position) { const coordinates = locationCoordinates(position); if (!coordinates) { handleLocationError(); return; } - filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; + filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = ''; filterState.stationLocations = []; selectedEntity = null; const detailCard = document.getElementById('mapDetailCard'); if (detailCard) detailCard.hidden = true; diff --git a/vehicle-map/tests/test_app.mjs b/vehicle-map/tests/test_app.mjs index 0ddd81c4..653c3579 100644 --- a/vehicle-map/tests/test_app.mjs +++ b/vehicle-map/tests/test_app.mjs @@ -121,6 +121,13 @@ filterState.query = ''; filterState.province = '广东省'; filterState.city = '广州市'; filterState.district = '黄埔区'; assert.deepEqual(filteredStations().map(station => station.id), ['GD-1']); filterState.province = ''; filterState.city = ''; filterState.district = ''; +filterState.stationLocations = [ + { province: '广东省', city: '广州市', district: '' }, + { province: '浙江省', city: '嘉兴市', district: '' } +]; +assert.deepEqual(filteredStations().map(station => station.id).sort(), ['GD-1', 'ZJ-1']); +assert.equal(stationLocationFilterLabel(filterState.stationLocations[0]), '广州'); +filterState.stationLocations = []; assert.ok(Math.abs(distanceKm([113, 23], [114, 23]) - 102.4) < 1); assert.equal(locationCoordinates({ coords: { longitude: NaN, latitude: 23 } }), null); map = {