mirror of
https://github.com/wahyd4/ai-projects.git
synced 2026-08-08 20:56:10 +10:00
Init map
This commit is contained in:
@@ -0,0 +1 @@
|
||||
3.12
|
||||
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from database import init_db, add_country, remove_country, get_all_countries, save_highlight_color, get_highlight_color
|
||||
import requests
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # 启用CORS
|
||||
init_db()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
app.logger.info('访问主页')
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/countries', methods=['GET'])
|
||||
def get_countries():
|
||||
app.logger.info('获取国家列表')
|
||||
countries = get_all_countries()
|
||||
app.logger.info(f'返回的国家列表: {countries}')
|
||||
return jsonify(countries)
|
||||
|
||||
@app.route('/add_country', methods=['POST'])
|
||||
def add_country_route():
|
||||
app.logger.info('添加国家')
|
||||
country = request.json['country']
|
||||
app.logger.info(f'尝试添加国家: {country}')
|
||||
# 验证国家名称
|
||||
response = requests.get(f"https://restcountries.com/v3.1/name/{country}")
|
||||
if response.status_code == 200:
|
||||
add_country(country)
|
||||
app.logger.info(f'成功添加国家: {country}')
|
||||
return jsonify({"success": True, "message": "国家添加成功"})
|
||||
else:
|
||||
app.logger.warning(f'无效的国家名称: {country}')
|
||||
return jsonify({"success": False, "message": "无效的国家名称"}), 400
|
||||
|
||||
@app.route('/remove_country', methods=['POST'])
|
||||
def remove_country_route():
|
||||
app.logger.info('删除国家')
|
||||
country = request.json['country']
|
||||
remove_country(country)
|
||||
return jsonify({"success": True})
|
||||
|
||||
@app.route('/test')
|
||||
def test():
|
||||
return "测试路由正常工作"
|
||||
|
||||
@app.route('/save_highlight_color', methods=['POST'])
|
||||
def save_highlight_color_route():
|
||||
color = request.json['color']
|
||||
save_highlight_color(color)
|
||||
return jsonify({"success": True})
|
||||
|
||||
@app.route('/get_highlight_color', methods=['GET'])
|
||||
def get_highlight_color_route():
|
||||
color = get_highlight_color()
|
||||
return jsonify({"color": color})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, port=5001)
|
||||
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
import sqlite3
|
||||
|
||||
def init_db():
|
||||
conn = sqlite3.connect('countries.db')
|
||||
c = conn.cursor()
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS countries
|
||||
(name TEXT PRIMARY KEY)''')
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS settings
|
||||
(key TEXT PRIMARY KEY, value TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def add_country(country):
|
||||
conn = sqlite3.connect('countries.db')
|
||||
c = conn.cursor()
|
||||
c.execute("INSERT OR IGNORE INTO countries (name) VALUES (?)", (country,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def remove_country(country):
|
||||
conn = sqlite3.connect('countries.db')
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM countries WHERE name = ?", (country,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_all_countries():
|
||||
conn = sqlite3.connect('countries.db')
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT name FROM countries")
|
||||
countries = [row[0] for row in c.fetchall()]
|
||||
conn.close()
|
||||
return countries
|
||||
|
||||
def save_highlight_color(color):
|
||||
conn = sqlite3.connect('countries.db')
|
||||
c = conn.cursor()
|
||||
c.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ('highlight_color', color))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_highlight_color():
|
||||
conn = sqlite3.connect('countries.db')
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT value FROM settings WHERE key = 'highlight_color'")
|
||||
result = c.fetchone()
|
||||
conn.close()
|
||||
return result[0] if result else None
|
||||
@@ -0,0 +1,4 @@
|
||||
Flask==2.3.2
|
||||
Werkzeug==2.3.6
|
||||
requests==2.26.0
|
||||
flask-cors==3.0.10
|
||||
@@ -0,0 +1,51 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#country-list {
|
||||
width: 300px;
|
||||
padding: 20px;
|
||||
background-color: #f0f0f0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#map { /* 将 #globe 改为 #map */
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#color-picker {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-box {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -0,0 +1,369 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
|
||||
let scene, camera, renderer, map, controls;
|
||||
const countries = [];
|
||||
const highlightedCountries = [];
|
||||
let allCountries = []; // 存储所有国家名称
|
||||
let worldData; // 存储世界地理数据
|
||||
let worldDataLoaded = false; // 标记世界数据是否已加载
|
||||
let highlightColor = 0xFFFF00; // 默认高亮颜色
|
||||
|
||||
export { init, addCountry, removeCountry, exportMap };
|
||||
|
||||
function init() {
|
||||
if (scene) {
|
||||
console.log('Scene already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
scene = new THREE.Scene();
|
||||
const aspect = (window.innerWidth - 300) / window.innerHeight;
|
||||
const frustumSize = 180;
|
||||
camera = new THREE.OrthographicCamera(
|
||||
frustumSize * aspect / -2, frustumSize * aspect / 2,
|
||||
frustumSize / 2, frustumSize / -2,
|
||||
0.1, 1000
|
||||
);
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(window.innerWidth - 300, window.innerHeight);
|
||||
document.getElementById('map').appendChild(renderer.domElement);
|
||||
|
||||
const mapTexture = new THREE.TextureLoader().load('/static/images/world.topo.bathy.200412.3x5400x2700.jpg');
|
||||
const material = new THREE.MeshBasicMaterial({ map: mapTexture });
|
||||
map = new THREE.Mesh(new THREE.PlaneGeometry(360, 180), material);
|
||||
scene.add(map);
|
||||
|
||||
camera.position.z = 100;
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableRotate = false;
|
||||
controls.enablePan = true;
|
||||
controls.enableZoom = true;
|
||||
controls.panSpeed = 0.5; // 调整平移速度
|
||||
controls.zoomSpeed = 0.5; // 调整缩放速度
|
||||
controls.screenSpacePanning = true; // 添加这行
|
||||
controls.mouseButtons = {
|
||||
LEFT: THREE.MOUSE.PAN,
|
||||
MIDDLE: THREE.MOUSE.DOLLY,
|
||||
RIGHT: THREE.MOUSE.ROTATE
|
||||
}; // 添加这个配置
|
||||
|
||||
setupColorPicker();
|
||||
loadHighlightColor(); // 从服务器加载保存的高亮颜色
|
||||
|
||||
loadWorldData();
|
||||
animate();
|
||||
loadCountries();
|
||||
loadAllCountries();
|
||||
setupAutocomplete();
|
||||
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
const aspect = (window.innerWidth - 300) / window.innerHeight;
|
||||
const frustumSize = 180;
|
||||
camera.left = -frustumSize * aspect / 2;
|
||||
camera.right = frustumSize * aspect / 2;
|
||||
camera.top = frustumSize / 2;
|
||||
camera.bottom = -frustumSize / 2;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth - 300, window.innerHeight);
|
||||
}
|
||||
|
||||
function loadWorldData() {
|
||||
fetch('https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
worldData = data;
|
||||
console.log('Loaded country names:', data.features.map(f => ({
|
||||
ADMIN: f.properties.ADMIN,
|
||||
NAME: f.properties.NAME,
|
||||
SOVEREIGNT: f.properties.SOVEREIGNT
|
||||
})));
|
||||
drawCountryBorders();
|
||||
worldDataLoaded = true;
|
||||
updateHighlights();
|
||||
loadAllCountries();
|
||||
setupAutocomplete();
|
||||
});
|
||||
}
|
||||
|
||||
function drawCountryBorders() {
|
||||
const material = new THREE.LineBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.3 });
|
||||
worldData.features.forEach(feature => {
|
||||
if (feature.geometry.type === "Polygon") {
|
||||
drawPolygon(feature.geometry.coordinates[0], material);
|
||||
} else if (feature.geometry.type === "MultiPolygon") {
|
||||
feature.geometry.coordinates.forEach(polygon => {
|
||||
drawPolygon(polygon[0], material);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawPolygon(coords, material) {
|
||||
const points = coords.map(coord => new THREE.Vector3(coord[0], coord[1], 0.1));
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const line = new THREE.Line(geometry, material);
|
||||
scene.add(line);
|
||||
}
|
||||
|
||||
function highlightCountry(countryName) {
|
||||
if (!worldDataLoaded) {
|
||||
console.log('World data not loaded yet. Skipping highlight for:', countryName);
|
||||
return;
|
||||
}
|
||||
const aliasName = getCountryAlias(countryName);
|
||||
const feature = worldData.features.find(f => {
|
||||
const admin = (f.properties.ADMIN || '').toLowerCase();
|
||||
const name = (f.properties.NAME || '').toLowerCase();
|
||||
const sovereignt = (f.properties.SOVEREIGNT || '').toLowerCase();
|
||||
const aliasLower = aliasName.toLowerCase();
|
||||
return admin === aliasLower || name === aliasLower || sovereignt === aliasLower ||
|
||||
admin.includes(aliasLower) || name.includes(aliasLower) || sovereignt.includes(aliasLower);
|
||||
});
|
||||
if (feature) {
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: highlightColor,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
depthTest: false
|
||||
});
|
||||
if (feature.geometry.type === "Polygon") {
|
||||
const mesh = createPolygonMesh(feature.geometry.coordinates[0], material);
|
||||
mesh.name = countryName;
|
||||
scene.add(mesh);
|
||||
highlightedCountries.push(mesh);
|
||||
} else if (feature.geometry.type === "MultiPolygon") {
|
||||
feature.geometry.coordinates.forEach(polygon => {
|
||||
const mesh = createPolygonMesh(polygon[0], material);
|
||||
mesh.name = countryName;
|
||||
scene.add(mesh);
|
||||
highlightedCountries.push(mesh);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('Country not found in world data:', countryName, 'Alias:', aliasName);
|
||||
}
|
||||
}
|
||||
|
||||
function createPolygonMesh(coords, material) {
|
||||
const shape = new THREE.Shape();
|
||||
coords.forEach((coord, index) => {
|
||||
if (index === 0) {
|
||||
shape.moveTo(coord[0], coord[1]);
|
||||
} else {
|
||||
shape.lineTo(coord[0], coord[1]);
|
||||
}
|
||||
});
|
||||
const geometry = new THREE.ShapeGeometry(shape);
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.z = 0.1;
|
||||
return mesh;
|
||||
}
|
||||
|
||||
function addCountry() {
|
||||
const input = document.getElementById('new-country');
|
||||
const country = input.value.trim();
|
||||
if (country) {
|
||||
fetch('/add_country', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ country }),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
loadCountries();
|
||||
input.value = '';
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error adding country:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeCountry(country) {
|
||||
fetch('/remove_country', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ country }),
|
||||
})
|
||||
.then(() => {
|
||||
loadCountries();
|
||||
});
|
||||
}
|
||||
|
||||
function loadCountries() {
|
||||
fetch('/countries')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
countries.length = 0;
|
||||
countries.push(...data);
|
||||
updateCountryList();
|
||||
if (worldDataLoaded) {
|
||||
updateHighlights();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateCountryList() {
|
||||
const ul = document.getElementById('countries');
|
||||
ul.innerHTML = '';
|
||||
countries.forEach(country => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'flex justify-between items-center bg-gray-100 p-2 rounded';
|
||||
const span = document.createElement('span');
|
||||
span.textContent = country;
|
||||
li.appendChild(span);
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.textContent = '删除';
|
||||
removeButton.className = 'bg-red-500 text-white px-2 py-1 rounded text-sm hover:bg-red-600 transition duration-200';
|
||||
removeButton.onclick = () => removeCountry(country);
|
||||
li.appendChild(removeButton);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update(); // 确保这行存在
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
function updateHighlights() {
|
||||
if (!worldDataLoaded) {
|
||||
console.log('World data not loaded yet. Skipping update highlights.');
|
||||
return;
|
||||
}
|
||||
// 清除之前的高亮
|
||||
highlightedCountries.forEach(mesh => {
|
||||
scene.remove(mesh);
|
||||
});
|
||||
highlightedCountries.length = 0;
|
||||
|
||||
// 重新高亮所有国家
|
||||
countries.forEach(country => {
|
||||
highlightCountry(country);
|
||||
});
|
||||
}
|
||||
|
||||
function getCountryAlias(countryName) {
|
||||
const aliases = {
|
||||
"United States": "United States of America",
|
||||
"USA": "United States of America",
|
||||
"US": "United States of America",
|
||||
"UK": "United Kingdom",
|
||||
"Britain": "United Kingdom",
|
||||
"Great Britain": "United Kingdom",
|
||||
"Russia": "Russian Federation",
|
||||
"China": "China",
|
||||
"People's Republic of China": "China",
|
||||
"Taiwan": "Taiwan",
|
||||
"North Korea": "Korea, Democratic People's Republic of",
|
||||
"South Korea": "Korea, Republic of",
|
||||
// 可以根据需要添加更多别名
|
||||
};
|
||||
return aliases[countryName] || countryName;
|
||||
}
|
||||
|
||||
function loadAllCountries() {
|
||||
fetch('https://restcountries.com/v3.1/all')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
allCountries = data.map(country => country.name.common).sort();
|
||||
updateCountrySuggestions(allCountries);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading all countries:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCountrySuggestions(suggestions) {
|
||||
const datalist = document.getElementById('country-suggestions');
|
||||
datalist.innerHTML = '';
|
||||
suggestions.forEach(country => {
|
||||
const option = document.createElement('option');
|
||||
option.value = country;
|
||||
datalist.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function setupAutocomplete() {
|
||||
const input = document.getElementById('new-country');
|
||||
input.addEventListener('input', function() {
|
||||
const value = this.value.toLowerCase();
|
||||
const filteredCountries = allCountries.filter(country =>
|
||||
country.toLowerCase().startsWith(value)
|
||||
);
|
||||
updateCountrySuggestions(filteredCountries);
|
||||
});
|
||||
}
|
||||
|
||||
function setupColorPicker() {
|
||||
const colors = [0xFFFF00, 0xFF0000, 0x00FF00, 0x0000FF, 0xFF00FF, 0x00FFFF];
|
||||
const colorPicker = document.getElementById('color-picker');
|
||||
colors.forEach(color => {
|
||||
const colorBox = document.createElement('div');
|
||||
colorBox.className = 'w-8 h-8 rounded-full cursor-pointer border-2 border-gray-300 hover:border-gray-500 transition duration-200';
|
||||
colorBox.style.backgroundColor = '#' + color.toString(16).padStart(6, '0');
|
||||
colorBox.addEventListener('click', () => setHighlightColor(color));
|
||||
colorPicker.appendChild(colorBox);
|
||||
});
|
||||
}
|
||||
|
||||
function setHighlightColor(color) {
|
||||
highlightColor = color;
|
||||
updateHighlights();
|
||||
saveHighlightColor(color);
|
||||
}
|
||||
|
||||
function loadHighlightColor() {
|
||||
fetch('/get_highlight_color')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.color) {
|
||||
highlightColor = parseInt(data.color, 16);
|
||||
updateHighlights();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveHighlightColor(color) {
|
||||
fetch('/save_highlight_color', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ color: color.toString(16) }),
|
||||
});
|
||||
}
|
||||
|
||||
function exportMap() {
|
||||
// Render the scene
|
||||
renderer.render(scene, camera);
|
||||
|
||||
// Convert the rendered image to a data URL
|
||||
const dataURL = renderer.domElement.toDataURL('image/png');
|
||||
|
||||
// Create a temporary link element
|
||||
const link = document.createElement('a');
|
||||
link.href = dataURL;
|
||||
link.download = 'world_map.png';
|
||||
|
||||
// Programmatically click the link to trigger the download
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh" class="h-full bg-gray-100">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>我去过的国家</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://unpkg.com/three@0.138.0/build/three.module.js",
|
||||
"three/examples/jsm/controls/OrbitControls": "https://unpkg.com/three@0.138.0/examples/jsm/controls/OrbitControls.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<div class="flex h-full">
|
||||
<div class="w-80 bg-white shadow-lg p-6 overflow-y-auto flex flex-col">
|
||||
<h2 class="text-2xl font-bold mb-4">国家列表</h2>
|
||||
<ul id="countries" class="mb-4 space-y-2 flex-grow overflow-y-auto"></ul>
|
||||
<div class="mb-4">
|
||||
<input type="text" id="new-country" placeholder="输入国家名称" list="country-suggestions" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<datalist id="country-suggestions"></datalist>
|
||||
</div>
|
||||
<button id="add-country-btn" class="w-full bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition duration-200 mb-4">添加国家</button>
|
||||
<p id="error-message" class="text-red-500 mt-2 mb-4"></p>
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold mb-2">高亮颜色</h3>
|
||||
<div id="color-picker" class="flex justify-around"></div>
|
||||
</div>
|
||||
<button id="export-btn" class="w-full bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 transition duration-200">导出地图</button>
|
||||
</div>
|
||||
<div id="map" class="flex-grow"></div>
|
||||
</div>
|
||||
<script type="module">
|
||||
import { addCountry, removeCountry, init, exportMap } from "{{ url_for('static', filename='js/globe.js') }}";
|
||||
window.addCountry = addCountry;
|
||||
window.removeCountry = removeCountry;
|
||||
window.exportMap = exportMap;
|
||||
|
||||
document.getElementById('add-country-btn').addEventListener('click', addCountry);
|
||||
document.getElementById('export-btn').addEventListener('click', exportMap);
|
||||
|
||||
// 在页面加载完成后初始化地图
|
||||
window.addEventListener('load', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user