feat: migrate to OpenLayers for map display
Some checks are pending
Docker / docker (push) Waiting to run

- Replaced Leaflet with OpenLayers for improved map rendering
- Added OpenLayers CSS and removed Leaflet CSS
- Updated map controller to use OpenLayers API
- Added marker icon in public directory
- Added scopes and associations for weather art in City model

This change migrates the map display from Leaflet to
OpenLayers, providing better performance and more features.
It also introduces new model associations for weather arts,
allowing to sort cities by latest weather updates.
This commit is contained in:
songtianlun 2025-02-15 00:19:04 +08:00
parent df456d1031
commit 9ac7dd46af
7 changed files with 244 additions and 49 deletions

View File

@ -1,5 +1,6 @@
@import "photoswipe/dist/photoswipe.css";
/*@import "leaflet/dist/leaflet.css";*/
@import 'ol/ol.css';
@tailwind base;
@tailwind components;
@ -42,4 +43,30 @@
.animate-spin {
animation: spin 1s linear infinite;
transform-origin: center center;
}
.map-container {
width: 100%;
height: 400px;
position: relative;
}
/* 使弹出窗口在地图上可见 */
.ol-overlay-container {
will-change: transform;
position: absolute;
transform: translate(-50%, -100%);
pointer-events: auto;
}
/* 添加一个小箭头 */
.ol-overlay-container::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -8px;
border-width: 8px;
border-style: solid;
border-color: white transparent transparent transparent;
}

View File

@ -1,11 +1,15 @@
import { Controller } from "@hotwired/stimulus"
import L from 'leaflet'
import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
import { fromLonLat } from 'ol/proj'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import { Vector as VectorLayer } from 'ol/layer'
import { Vector as VectorSource } from 'ol/source'
import { Style, Icon } from 'ol/style'
import 'leaflet/dist/leaflet.css'
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
// import iconRetinaUrl from '../images/leaflet/marker-icon-2x.png';
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
export default class extends Controller {
static values = {
@ -13,46 +17,45 @@ export default class extends Controller {
longitude: Number
}
init() {
}
connect() {
delete L.Icon.Default.prototype._getIconUrl
L.Icon.Default.mergeOptions({
// iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
// iconUrl: require('leaflet/dist/images/marker-icon.png'),
// shadowUrl: require('leaflet/dist/images/marker-shadow.png')
iconRetinaUrl: iconRetinaUrl,
iconUrl: iconUrl,
shadowUrl: shadowUrl,
});
this.initializeMap()
}
initializeMap() {
// 创建地图实例
this.map = L.map(this.element).setView(
[this.latitudeValue, this.longitudeValue],
4 // 缩放级别
)
const map = new Map({
target: this.element,
layers: [
new TileLayer({
source: new OSM()
})
],
view: new View({
center: fromLonLat([this.longitudeValue, this.latitudeValue]),
zoom: 4
})
})
// 添加地图图层
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map)
// 添加标记点
const marker = new Feature({
geometry: new Point(fromLonLat([this.longitudeValue, this.latitudeValue]))
})
// 添加标记
L.marker([this.latitudeValue, this.longitudeValue])
.addTo(this.map)
.bindPopup(document.querySelector('h1').textContent)
.openPopup()
}
const vectorSource = new VectorSource({
features: [marker]
})
disconnect() {
if (this.map) {
this.map.remove()
}
const vectorLayer = new VectorLayer({
source: vectorSource,
style: new Style({
image: new Icon({
anchor: [0.5, 1],
src: '/marker-icon.svg', // 确保在public目录下有这个图标
scale: 1.5
})
})
})
map.addLayer(vectorLayer)
}
}

View File

@ -107,6 +107,34 @@ class City < ApplicationRecord
)
}
# 定义 latest_weather_art 关联
has_one :latest_weather_art, -> { order(weather_date: :desc) },
class_name: 'WeatherArt'
# 包含最新天气艺术的 scope
scope :with_latest_weather_art, -> {
includes(:latest_weather_art)
}
# 只获取有最新天气艺术的城市
scope :has_weather_art, -> {
joins(:weather_arts).distinct
}
# 按最新天气更新时间排序
scope :order_by_latest_weather, -> {
joins(:weather_arts)
.group('cities.id')
.order('MAX(weather_arts.weather_date) DESC')
}
# 获取最近24小时内更新过天气的城市
scope :recently_updated, -> {
joins(:weather_arts)
.where('weather_arts.weather_date > ?', 24.hours.ago)
.distinct
}
def to_s
name

View File

@ -1,8 +1,14 @@
<div class="mt-8">
<div
data-controller="map"
data-map-latitude-value="<%= city.latitude %>"
data-map-longitude-value="<%= city.longitude %>"
class="w-full h-[400px] rounded-lg shadow-lg">
<!-- 在你想要显示地图的位置添加以下代码 -->
<div class="mt-2">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<!-- <h3 class="card-title">Location Map</h3>-->
<div
data-controller="map"
data-map-latitude-value="<%= @city.latitude %>"
data-map-longitude-value="<%= @city.longitude %>"
class="w-full h-[400px] rounded-lg overflow-hidden">
</div>
</div>
</div>
</div>

View File

@ -18,7 +18,7 @@
"autoprefixer": "^10.4.20",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.1",
"leaflet": "^1.9.4",
"ol": "^10.4.0",
"photoswipe": "^5.4.4",
"postcss": "^8.5.1",
"sass": "^1.83.4",

3
public/marker-icon.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FF4444">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

136
yarn.lock
View File

@ -323,6 +323,11 @@
"@parcel/watcher-win32-ia32" "2.5.0"
"@parcel/watcher-win32-x64" "2.5.0"
"@petamoriken/float16@^3.4.7":
version "3.9.1"
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.9.1.tgz#f1239d6721f25c1bf921dde0d7d678efef9c1de2"
integrity sha512-j+ejhYwY6PeB+v1kn7lZFACUIG97u90WxMuGosILFsl9d4Ovi0sjk0GlPfoEcx+FzvXZDAfioD+NGnnPamXgMA==
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
@ -333,6 +338,11 @@
resolved "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.2.201.tgz"
integrity sha512-wsTdWoZ5EfG5k3t7ORdyQF0ZmDEgN4aVPCanHAiNEwCROqibSZMXXmCbH7IDJUVri4FOeAVwwbPINI7HVHPKBw==
"@types/rbush@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-4.0.0.tgz#b327bf54952e9c924ea6702c36904c2ce1d47f35"
integrity sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
@ -458,11 +468,36 @@ color-convert@^2.0.1:
dependencies:
color-name "~1.1.4"
color-name@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.0.0.tgz#03ff6b1b5aec9bb3cf1ed82400c2790dfcd01d2d"
integrity sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-parse@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-2.0.2.tgz#37b46930424924060988edf25b24e6ffb4a1dc3f"
integrity sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==
dependencies:
color-name "^2.0.0"
color-rgba@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-3.0.0.tgz#77090bdcdb2951c1735e20099ddd50401675375b"
integrity sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==
dependencies:
color-parse "^2.0.0"
color-space "^2.0.0"
color-space@^2.0.0, color-space@^2.0.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/color-space/-/color-space-2.3.1.tgz#1c13ca8ee017585807e65c76de3870cdc7c4fdfb"
integrity sha512-5DJdKYwoDji3ik/i0xSn+SiwXsfwr+1FEcCMUz2GS5speGCfGSbBMOLd84SDUBOuX8y4CvdFJmOBBJuC4wp7sQ==
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
@ -520,6 +555,11 @@ dlv@^1.1.3:
resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz"
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
earcut@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.1.tgz#f60b3f671c5657cca9d3e131c5527c5dde00ef38"
integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
@ -629,6 +669,20 @@ function-bind@^1.1.2:
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
geotiff@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/geotiff/-/geotiff-2.1.3.tgz#993f40f2aa6aa65fb1e0451d86dd22ca8e66910c"
integrity sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==
dependencies:
"@petamoriken/float16" "^3.4.7"
lerc "^3.0.0"
pako "^2.0.4"
parse-headers "^2.0.2"
quick-lru "^6.1.1"
web-worker "^1.2.0"
xml-utils "^1.0.2"
zstddec "^0.1.0"
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
@ -739,10 +793,10 @@ jquery-ujs@^1.2.2:
resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz"
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
leaflet@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
lerc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lerc/-/lerc-3.0.0.tgz#36f36fbd4ba46f0abf4833799fff2e7d6865f5cb"
integrity sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==
lilconfig@^3.0.0, lilconfig@^3.1.3:
version "3.1.3"
@ -828,11 +882,34 @@ object-hash@^3.0.0:
resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
ol@^10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/ol/-/ol-10.4.0.tgz#b073dd2b08c3cc31fece1c904a6711e0d289d6e4"
integrity sha512-gv3voS4wgej1WVvdCz2ZIBq3lPWy8agaf0094E79piz8IGQzHiOWPs2in1pdoPmuTNvcqGqyUFG3IbxNE6n08g==
dependencies:
"@types/rbush" "4.0.0"
color-rgba "^3.0.0"
color-space "^2.0.1"
earcut "^3.0.0"
geotiff "^2.1.3"
pbf "4.0.1"
rbush "^4.0.0"
package-json-from-dist@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz"
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
pako@^2.0.4:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
parse-headers@^2.0.2:
version "2.0.5"
resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.5.tgz#069793f9356a54008571eb7f9761153e6c770da9"
integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
@ -851,6 +928,13 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
pbf@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pbf/-/pbf-4.0.1.tgz#ad9015e022b235dcdbe05fc468a9acadf483f0d4"
integrity sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==
dependencies:
resolve-protobuf-schema "^2.1.0"
photoswipe@^5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.4.tgz#e045dc036453493188d5c8665b0e8f1000ac4d6e"
@ -929,11 +1013,33 @@ postcss@^8.4.47, postcss@^8.5.1:
picocolors "^1.1.1"
source-map-js "^1.2.1"
protocol-buffers-schema@^3.3.1:
version "3.6.0"
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03"
integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-lru@^6.1.1:
version "6.1.2"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-6.1.2.tgz#e9a90524108629be35287d0b864e7ad6ceb3659e"
integrity sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==
quickselect@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603"
integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==
rbush@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/rbush/-/rbush-4.0.1.tgz#1f55afa64a978f71bf9e9a99bc14ff84f3cb0d6d"
integrity sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==
dependencies:
quickselect "^3.0.0"
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz"
@ -953,6 +1059,13 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
resolve-protobuf-schema@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758"
integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==
dependencies:
protocol-buffers-schema "^3.3.1"
resolve@^1.1.7, resolve@^1.22.8:
version "1.22.10"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz"
@ -1140,6 +1253,11 @@ util-deprecate@^1.0.2:
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
web-worker@^1.2.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5"
integrity sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
@ -1165,7 +1283,17 @@ wrap-ansi@^8.1.0:
string-width "^5.0.1"
strip-ansi "^7.0.1"
xml-utils@^1.0.2:
version "1.10.1"
resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.10.1.tgz#fa0c9b38545760532d4cf89003f90c3b24e7200f"
integrity sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ==
yaml@^2.3.4:
version "2.7.0"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz"
integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==
zstddec@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.1.0.tgz#7050f3f0e0c3978562d0c566b3e5a427d2bad7ec"
integrity sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==