答案1
如果你想将它用于你的作业的话,我认为这是一个迟来的答案……
代码基于一组(大量)点。我有一个小的 Python 程序,从地图开始,我可以用它来构造边界点。我应该将它分别用于地图的每个区域,但这个想法来得太晚了。因此,结果只是一组无序的点,除了大陆边界之外。
\documentclass[margin=1in]{standalone}
\usepackage{tikz}
\usetikzlibrary{math, calc}
\begin{document}
\tikzmath{%
coordinate \A{1}, \A{2}, \B{1}, \B{2}, \T{1}, \T{2};
coordinate \C, \D, \E, \F, \G, \H, \I;
\A{1} = (111pt, -441pt);
\A{2} = (133pt, -458pt);
\A{3} = (160pt, -463pt);
\A{4} = (189pt, -437pt);
\A{5} = (243pt, -438pt);
\A{6} = (281pt, -405pt);
\A{7} = (319pt, -397pt);
\A{8} = (362pt, -393pt);
\A{9} = (393pt, -404pt);
\A{10} = (422pt, -458pt);
\A{11} = (450pt, -428pt);
\A{12} = (444pt, -459pt);
\A{13} = (458pt, -455pt);
\A{14} = (464pt, -472pt);
\A{15} = (475pt, -471pt);
\A{16} = (493pt, -522pt);
\A{17} = (543pt, -541pt);
\A{18} = (563pt, -524pt);
\A{19} = (581pt, -541pt);
\A{20} = (599pt, -530pt);
\A{21} = (604pt, -519pt);
\A{22} = (631pt, -516pt);
\A{23} = (652pt, -450pt);
\A{24} = (676pt, -405pt);
\A{25} = (694pt, -330pt);
\A{26} = (686pt, -279pt);
\A{27} = (668pt, -251pt);
\A{28} = (651pt, -237pt);
\A{29} = (648pt, -220pt);
\A{30} = (630pt, -208pt);
\A{31} = (621pt, -178pt);
\A{32} = (588pt, -158pt);
\A{33} = (565pt, -82pt);
\A{34} = (551pt, -73pt);
\A{35} = (544pt, -72pt);
\A{36} = (542pt, -42pt);
\A{37} = (522pt, -11pt);
\A{38} = (509pt, -55pt);
\A{39} = (509pt, -99pt);
\A{40} = (494pt, -128pt);
\A{41} = (474pt, -119pt);
\A{42} = (428pt, -85pt);
\A{43} = (442pt, -40pt);
\A{44} = (379pt, -24pt);
\A{45} = (349pt, -41pt);
\A{46} = (327pt, -76pt);
\A{47} = (290pt, -64pt);
\A{48} = (268pt, -84pt);
\A{49} = (247pt, -112pt);
\A{50} = (227pt, -116pt);
\A{51} = (210pt, -155pt);
\A{52} = (178pt, -175pt);
\A{53} = (138pt, -184pt);
\A{54} = (98pt, -212pt);
\A{55} = (92pt, -261pt);
\A{56} = (97pt, -300pt);
\A{57} = (123pt, -389pt);
\A{58} = (124pt, -420pt);
\A{59} = (111pt, -441pt);
\T{1} = (559pt, -583pt);
\T{2} = (585pt, -591pt);
\T{3} = (610pt, -585pt);
\T{4} = (612pt, -608pt);
\T{5} = (599pt, -634pt);
\T{6} = (592pt, -644pt);
\T{7} = (579pt, -644pt);
\T{8} = (566pt, -623pt);
\T{9} = (566pt, -608pt);
\T{10} = (559pt, -583pt);
\B{1} = (634pt, -509pt);
\B{2} = (614pt, -493pt);
\B{3} = (604pt, -479pt);
\B{4} = (570pt, -480pt);
\B{5} = (558pt, -482pt);
\B{6} = (537pt, -452pt);
\B{7} = (523pt, -447pt);
\B{8} = (504pt, -439pt);
\B{9} = (504pt, -337pt);
\B{10} = (620pt, -336pt);
\B{11} = (635pt, -328pt);
\B{12} = (651pt, -328pt);
\B{13} = (659pt, -337pt);
\B{14} = (694pt, -327pt);
\C = (504pt, -281pt);
\D = (460pt, -109pt);
\E = (461pt, -281pt);
\F = (324pt, -281pt);
\G = (324pt, -397pt);
\H = (504pt, -527pt);
\I = (324pt, -75pt);
}
\begin{tikzpicture}[xscale=.6, yscale=.55]
% australia
\draw (\A{1})
\foreach \i in {2, 3, ..., 59}{ -- (\A{\i}) } -- cycle;
% t
\draw (\T{1})
\foreach \i in {2, 3, ..., 10}{ -- (\T{\i}) } -- cycle;
% new s w
\draw (\B{1})
\foreach \i in {2, 3, ..., 14}{ -- (\B{\i}) }
-- (\A{25}) -- (\A{24}) -- (\A{23}) -- cycle;
% q
\draw (\B{14}) -- (\A{26})
\foreach \i in {27, ..., 41}{ -- (\A{\i}) }
-- (\D) -- (\E) -- (\C)
\foreach \i in {9, ..., 13}{ -- (\B{\i}) }
-- cycle;
% v
\draw (\B{1})
\foreach \i in {2, 3, ..., 9}{ -- (\B{\i}) }
-- (\H)
\foreach \i in {17, ..., 22}{ -- (\A{\i}) } -- cycle;
% s a
\draw (\H) -- (\C) -- (\F) -- (\G)
\foreach \i in {8, ..., 16}{ -- (\A{\i})} -- cycle;
% n t
\draw (\D) -- (\E) -- (\F) -- (\I)
\foreach \i in {46, ..., 42}{ -- (\A{\i})} -- cycle;
% w a
\draw (\G) -- (\I)
\foreach \i in {47, ..., 59}{ -- (\A{\i})}
\foreach \i in {1, ..., 7}{ -- (\A{\i})} -- cycle;
\end{tikzpicture}
\end{document}
我在下面添加了 Python 程序。执行时,它会启动一个带有两个轴的图形窗口:左侧带有按钮,右侧带有我们想要“绘制”的地图。最后两行适用于文件澳大利亚.jpg必须存在于同一目录中。使用它后,至少会创建一个文件“australia.dat”...
该程序处理仅有的一条连通曲线(我认为)可以随意丰富。但谁知道呢!
'''
Construct a closed polygonal curve that enclosed a continent (country
or whatever). Then, the curve is enriched with vertices such that,
at the limit it would coincide with the continent.
The curves (contour curves) can be put together to form an animation.
Rem. When launched, a jpg file must exist in the same directory.
'''
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib import patches
from matplotlib.widgets import Button
from matplotlib import animation
from matplotlib.animation import FuncAnimation
import os.path
import copy
import time
#####################################################################
class ContourCurves():
_fig = None # figure; contains buttons and axes for the map
_axes = None # Axes on which the contour curves are constructed
_bAxes = None # Axes for the buttons
buttonBox_axes_list = []
ve_list = []
'''
It is the list (ordered) of all the vertices and edges
correponsding to the last CCurve. It is necessary to the
edge identification when erasing when replaced by a chain.
The position of the edge in this list coincides with the
position of the corresponding 2dLine element in the list
_axes.lines.
The list is updated when a vertex or an edge is drawn.
But, when erasing an edge, the list and the _axes.lines
must be modified simultaneously.
'''
cidpress = None # connection identity, an integer
cidpress_status = False
tmp_color = 'blue'
img_array = None
def __init__(self, mapFile_name):
self.sequence = CCurvesSequence(mapFile_name, -1)
'''
sequence of contour curves, an object of CCurvesSequence
The second argument is not yet known. It will be moidified
at the end of the initialisation.
'''
self.level = max(self.sequence.dic.keys())
w_fig = 12.6
h_fig = 8.2
## bottons position constants
x_b = .05
y_b = .15
w_b = .15 # width
h_b = .09 # height
subplots_display = {'width_ratios': [1, 4], 'left':.05, 'right':.95,
'bottom':.03, 'top':.97, 'wspace':0.1}
'''
Extent of the subplots as a fraction of figure width or height.
Left cannot be larger than right, and bottom cannot be larger
than top.
In fact, right = left + axes width + wspaces between axes.
'''
fig, axes = plt.subplots(1, 2, gridspec_kw=subplots_display)
fig.set_size_inches(w_fig, h_fig)
fig.suptitle("Press 'Draw or enrich CCurve'",
horizontalalignment="left", x=.02)
axes[0].set(aspect='equal')
axes[0].set_frame_on(False)
axes[0].set_xlim(-1, 4)
axes[0].set_ylim(-1, 10)
axes[0].set_xticks([])
axes[0].set_yticks([])
buttonBox_axes = plt.axes([x_b, y_b, w_b, h_b])
closeButton = Button(buttonBox_axes, "Close")
closeButton.on_clicked(lambda x: plt.close("all"))
self.buttonBox_axes_list.append(buttonBox_axes)
buttonBox_axes = plt.axes([x_b, y_b+1.5*h_b, w_b, h_b])
drawCurveButton = Button(buttonBox_axes, "Draw or enrich CCurve")
drawCurveButton.on_clicked(self.callDrawCurve)
self.buttonBox_axes_list.append(buttonBox_axes)
buttonBox_axes = plt.axes([x_b, y_b+4.5*h_b, w_b, h_b])
saveCCSequenceButton = Button(buttonBox_axes, "Save CC sequence")
saveCCSequenceButton.on_clicked(self.callSaveCCSequence)
self.buttonBox_axes_list.append(buttonBox_axes)
buttonBox_axes = plt.axes([x_b, y_b+6*h_b, w_b, h_b])
animateSequenceButton = Button(buttonBox_axes, "Animate CC sequence")
animateSequenceButton.on_clicked(self.callAnimateCCSequence)
self.buttonBox_axes_list.append(buttonBox_axes)
## main axes
axes[1].set(aspect='equal')
axes[1].set_frame_on(False)
axes[1].set_xticks([])
axes[1].set_yticks([])
img_array = plt.imread(mapFile_name+".jpg") # 3 dim array
# print(np.shape(img_array)[0])
axes[1].imshow(img_array)
self._fig = fig
self._bAxes = axes[0]
self._axes = axes[1]
self.img_array = img_array
self.sequence.epsilon = max(np.shape(img_array))/(10*max(h_fig, w_fig))
# It sets the value of epsilon for the CCurvesSequence object.
plt.show()
### end __init__
def callDrawCurve(self, event):
'''
Connects the press button event with the figure (canvas).
The Draw curve button must be pressed for the curve to be
constructed or modified.
'''
plt.ion()
if self.sequence.dic[0]==[]:
title = "Draw the first CCurve; level=" + str(self.level)
else:
self.level = self.level + 1
'''
self.sequence must be modified. The previous level curves are
copied for the current level. They will be modified afterwards
through the method on_press().
'''
self.sequence.n_gives_nPlus1()
title = "Enrich the CCurves; level=" + str(self.level) + "\n" \
+ "(click on a vertex to continue)"
self.draw_previousCCurves()
self._fig._suptitle.set_text(title)
self.cidpress = self._fig.canvas.mpl_connect('button_press_event',
self.on_press)
### end callDrawCurve
def on_press(self, event):
'''
If the "press" is in _axes, then the contour curve is constructed.
If not, then nothing is done.
'''
# print('you pressed button', event.button, event.xdata, event.ydata)
if event.inaxes==self._axes:
x = event.xdata
y = event.ydata
to_draw, to_erase = self.sequence.ccs_nextEdge(x, y, self.level)
if to_draw["vertex"]:
self.draw_vertex(to_draw["vertex"])
if to_draw["edge"]:
self.draw_edge(*to_draw["edge"])
if to_erase["vertex"]: # a vertex
self.erase_vertex(to_erase["vertex"])
if to_erase["edge"]: # a list with two edges
for e in to_erase["edge"]:
self.erase_edge(e)
self._fig.canvas.draw_idle()
'''
Note that plt.draw() is not the same as fig.draw() if fig is a
figure. In order to draw a figure a renderer needs to be supplied.
'''
### end on_press
def callSaveCCSequence(self, event):
plt.ion()
self._fig._suptitle.set_text("Contour curves saved")
if self.cidpress_status:
self._fig.canvas.mpl_disconnect(self.cidpress)
self.cidpress_status = False
self.sequence.saveSequence()
return None
### end callSaveCCSequence
def draw_previousCCurves(self):
k = max(self.sequence.dic.keys())
for L in self.sequence.dic[k]:
p = L[0] # the first vertex
for q in L[1:]:
self.draw_edge(p, q)
self.draw_vertex(q)
p = q
self._fig.canvas.draw_idle()
### end draw_previousCCurves
def draw_vertex(self, point):
self._axes.plot(point[0], point[1], marker='o',
markersiZe=4, color=self.tmp_color, ls='')
self.ve_list.append([point])
### end draw_vertex
def draw_edge(self, p, q):
self._axes.plot([p[0], q[0]], [p[1], q[1]], color=self.tmp_color)
self.ve_list.append([p, q])
### end draw_edge
def erase_vertex(self, point):
for L in self.ve_list:
if L==[point]:
i = self.ve_list.index(L)
del self.ve_list[i]
del self._axes.lines[i]
print('vertex has been erased')
### end erase_vertex
def erase_edge(self, edge):
for L in self.ve_list:
if set(L)==set(edge):
i = self.ve_list.index(L)
del self.ve_list[i]
del self._axes.lines[i]
print('edge has been erased')
### end erase_edge
def callAnimateCCSequence(self, event):
subplots_display = {'left':.05, 'right':.95, 'bottom':.06, 'top':.94}
fig, axes = plt.subplots(1, 1, gridspec_kw=subplots_display)
fig.set_size_inches(12, 8)
axes.set(aspect='equal')
axes.set_frame_on(True)
axes.set_xlim(0, np.shape(self.img_array)[0])
axes.set_ylim(-1, np.shape(self.img_array)[1])
XX = []
YY = []
slides_nb = len(self.sequence.dic.keys())
pymax = np.shape(self.img_array)[1]
print("py max =", pymax)
for k in self.sequence.dic.keys():
X = []
Y = []
for L in self.sequence.dic[k]:
x = [p[0] for p in L]
y = [2*pymax-p[1] for p in L] # horizontal flip
X.append(x)
Y.append(y)
XX.append(X)
YY.append(Y)
def animate(i):
for k in range(len(XX[i])):
plt.cla()
axes.plot(XX[i][k], YY[i][k])
axes.set_title('Map at step ' + str(i))
anim = FuncAnimation(fig, animate, interval=500, frames=slides_nb)
anim.save(self.sequence.file_name + '.mp4')
### end callAnimateCCSequence
### end Class ContourCurves
class CCurvesSequence():
xy_list = []
'''
It is the list of vertices of either a contour curve or of a
chain that replace an edge in a former contour curve.
'''
test_vertices = []
'''
The first element is the index of the component in the last
CC. The next argument is the origin of the enriched chain;
the next two are the vertices before and after this one in
the former contour curve (corresponding to the above index).
'''
def __init__(self, file_name, epsilon):
'''
epsilon is needed in the almostIdentical().
'''
self.file_name = file_name
self.epsilon = epsilon
D = {}
name = file_name + ".dat"
if os.path.isfile(name):
with open (name, "r") as f:
for line in f:
contents = line.strip().split("&") # type list
# print("text line ", contents)
if len(contents)==1:
if contents[0]!="component":
key = int(contents[0])
D[key] = []
else:
D[key].append([])
else:
point = (float(contents[0]), float(contents[1]))
D[key][-1].append(point)
else:
D[0] = []
self.dic = D
'''
The dictionary formed by the contour curves sequence; At
the beginning it has a single key (=0), its value being the
empty lilst.
'''
### end __init__
def n_gives_nPlus1(self):
'''
Introduces the key $n+1$ by giving it the value of the key
$n$ in the dictionary dic.
'''
n = max(self.dic.keys())
self.dic[n+1] = copy.deepcopy(self.dic[n])
### end n_gives_nPlus1
def ccs_nextEdge(self, x, y, level):
'''
Construct a level curve using the coordinates of each vertex
given by an in-axes 'click' in the ComponentCurves object.
The click in the neighborhood of the "initial point" closes
the action.
If level = 0:
"initial point" means "first point", and the component is
closed. It uses the method next_edge().
else:
"initial point" means one of the three vertices: first point
(the origin of the modification), next or previous point
(with respect to the initial).
Notice that in these later cases (next or previous), an edge
must be deleted in the graphical image of the ComponentCurves
object. In the first case, there is either a new component
that is inserted, or a the initial point that is deleted.
These operations must take place at the end of the "old edge"
modification.
It uses the method next_enrichedEdge().
'''
if level==0:
return self.next_edge(x, y) # level=0
else:
return self.next_enriched_edge(x, y, level)
### end ccs_nextEdge
def next_edge(self, x, y):
'''
Method invoked by ccs_nextEdge(). The output is a couple:
to-draw (a dictionary) and to_erase (a list representing an
"old" edge or an "old" vertex, i.e. a list with one element).
In this case, to_erase is always empty.
These two elements are passed to the ContourCurves object
through the use of ccs_nextEdge() of its attribute sequence.
Update self.dic by calling the method ccs_update() at the end.
'''
self.xy_list.append((x, y))
if len(self.xy_list)>1:
q = self.xy_list[-1]
p = self.xy_list[-2]
if self.almostIdentical(self.xy_list[0], q):
q = self.xy_list[0]
self.xy_list[-1] = q # the correct point in this case
to_draw = {"vertex":None, "edge":[p, q]}
self.ccs_update(self.xy_list, None)
self.xy_list = [] # a new connected component
else:
to_draw = {"vertex":q, "edge":[p, q]}
else:
to_draw = {"vertex":(x, y), "edge":None}
return to_draw, {"vertex":None, "edge":[]}
### end next_edge
def next_enriched_edge(self, x, y, key):
'''
Method invoked by ccs_nextEdge(). The output is a couple:
to-draw (a dictionary) and to_erase (a list representing an
"old" edge or an "old" vertex, i.e. a list with one element).
It is non-empty either when the end of the edge modification
is reached, i.e. the edge is replaced by the chain, or when
the chain has the form [v, v] (clicked on the same vertex
twice); the vertex is erased together with the two edges
issued from it.
These two elements are passed to the ContourCurves object
through the use of ccs_nextEdge() of its attribut sequence.
Update self.dic by calling the method ccs_update() at the end.
'''
to_draw = {"vertex":None, "edge":None}
to_erase = {"vertex":None, "edge":[]}
if self.xy_list==[]:
p, component_index = self.lookfor_closest_vertex((x, y), key)
if p:
self.xy_list.append(p)
self.test_vertices_method(p, component_index, key)
else:
'''
self.test_vertices contains the initial vertex and its
neighborhoods. There are four cases depending on the
positon of (x,y) with respect to these vertices. When
(x,y) coincides with one of the self.test_vertices, then
self.dic is updated through ccs_update().
'''
q = (x, y)
cc_index = self.test_vertices[0] # index of the CCurve
before_v = self.test_vertices[1]
v = self.test_vertices[2]
after_v = self.test_vertices[3]
status = [cc_index, v]
# to do: clicking two times on the same point
if self.almostIdentical(q, before_v):
status.append("before")
p = self.xy_list[-1]
self.xy_list.append(before_v)
to_draw = {"vertex":None, "edge":[p, before_v]}
to_erase["edge"].append([before_v, v])
self.ccs_update(self.xy_list, status)
self.xy_list = [] # prepare a new edge modification
elif self.almostIdentical(q, after_v):
status.append("after")
p = self.xy_list[-1]
self.xy_list.append(after_v)
to_draw = {"vertex":None, "edge":[p, after_v]}
to_erase["edge"].append([v, after_v])
self.ccs_update(self.xy_list, status)
self.xy_list = []
elif self.almostIdentical(q, v): # origin
status.append("on origin")
'''
There are two cases; when xy_list contains more than
one element, and when it is reduced to the origin.
'''
if len(self.xy_list)>1:
status.append("new component")
print(status)
p = self.xy_list[-1]
self.xy_list.append(v)
to_draw = {"vertex":None, "edge":[p, v]}
else:
status.append("delete")
print(status)
to_draw = {"vertex":None, "edge":[before_v, after_v]}
to_erase = {"vertex":v,
"edge":[[before_v, v], [v, after_v]]}
self.ccs_update(self.xy_list, status)
self.xy_list = []
else:
p = self.xy_list[-1]
self.xy_list.append(q)
to_draw = {"vertex":q, "edge":[p, q]}
return to_draw, to_erase
### end next_enriched_edge
def ccs_update(self, points_list, status):
'''
Update the self.dic dictionary using the points_list.
The items of self.dic are key:L, with L a list of lists.
Each element of L is given by a list of vertices. There
might be islands!
The behavior is different whether the maximal key is 0 or
not.
If the maximal key is 0, then status plays no role; the
points_list is the list of vertices of a connected component,
a ccurve (contour curve)
If the maximal key is >0, then either an edge is modified or a
vertex from a contour curve corresponding to the maximal key.
This edge is replaced by the edges of points_list. If a
vertex (origin of the chain) must be modified, then... (to do)
Desciption of status when not None; it is one of the lists
[component_index, origin, "on origin", "new component"]
[component_index, origin, "on origin", "delete"]
[component_index, origin, "before"]
[component_index, origin, "after"]
'''
m = max(self.dic.keys())
if m==0:
self.dic[m].append(points_list)
else:
cc_index = status[0]
p = status[1] # origin vertex of the chain
cc = self.dic[m][cc_index]
i = cc.index(p) # index of the origin in CCurve
if len(status)==3:
if status[2]=="before":
points_list.reverse()
if i!=0:
L = cc[:i-1] + points_list + cc[i+1:]
else:
L = cc[:-2] + points_list
else:
L = cc[:i] + points_list + cc[i+2:]
self.dic[m][cc_index] = L # replaces the edge by the chain
else:
'''
Working on a vertex (the origin of the chain); the chain
points_list is a cycle and becomes a new CCurve, or the
vertex is removed. Now status has four elements.
'''
if status[3]=="new component":
self.dic[m].append(points_list)
# to do: moving the new origin!
else:
if i==0 or i==len(cc)-1:
L = cc[1:-2]
L.append(L[0])
else:
L = cc
L.remove(p)
self.dic[m][cc_index] = L # updates the component
return None
### end ccs_update
def saveSequence(self):
'''
Save the sequence of CCurves in the file file_name.dat.
'''
name = self.file_name + ".dat"
with open (name, "w+") as file:
for k in self.dic.keys():
file.write(str(k) + "\n")
for L in self.dic[k]:
file.write("component\n")
for p in L:
file.write("{:.2f}".format(p[0])
+ " & " + "{:.2f}".format(p[1]) + "\n")
return print("The sequence of CCurves has been saved.")
### emd saveSequence
def test_vertices_method(self, vertex, component_index, key):
'''
Updates the test_vertices attribute.
'''
L = self.dic[key][component_index]
i = L.index(vertex)
if i==0:
self.test_vertices = [component_index, L[-2], L[0], L[1]]
else:
self.test_vertices = [component_index, L[i-1], L[i], L[i+1]]
### end test_vertices_method
def lookfor_closest_vertex(self, q, key):
for L in self.dic[key]:
for p in L:
if self.almostIdentical(p, q):
return p, self.dic[key].index(L)
return None, None
### end lookfor_closest_vertex
def almostIdentical(self, point1, point2):
'''
Determines (boolean) if two points are sufficiently close
to consider that they are identical. The condition should
depend on the image scale, or zoom... coming from the
ContourCurves object.
'''
dx = point1[0]-point2[0]
dy = point1[1]-point2[1]
e = self.epsilon
if dx<e and dx>-e and dy<e and dy>-e:
return True
else:
return False
### end almostIdentical
### end Class CCurvesSequence
##############################################################
##############################################################
file_name = "australia"
ContourCurves(file_name)